feat(observer): surface segment delivery freshness in list/status
Status and Last Seen reflect any beacon, including an empty status beacon that refreshes last_seen without a delivery because record_status_beacon sets last_seen before its all-None early return. Neither signal showed whether an observer was actually delivering segments. Persist last_segment_received_at and last_segment_day as a denormalization of the existing observer history records so list remains O(1) per row across the fleet.
All age rendering derives from last_segment_received_at. last_segment_day is contextual only and never drives an age calculation.
The compact age formatter floors units, so 19.5h renders 19h. Unknown segment freshness renders —, deliberately distinct from _fmt_time's never for unknown last_seen. Duplicate ingest remains unchanged because a duplicate is not a new delivery. reconcile_observers remains untouched because the new keys are top-level siblings of last_segment, not stats counters, so _aggregate_stats never sees them. There is no backfill: absent means unknown, and records self-populate on the next receipt.
The observer client contract manifest pins routes.py's sha256, so editing the receipt path required regeneration. Bump the bundle patch version from 2.1.8 to 2.1.9 because no wire surface changed: _serialize_observer is untouched, so the new keys are not exposed to clients.
Gates run:
- make format — exit 0
- make test-only TEST=tests/test_observer_cli.py — exit 0, 40 passed
- make test-app APP=observer — exit 0, 266 passed
- make check-layer-hygiene — exit 0
- make check-openapi — exit 0
- make ci — exit 0, 14661 passed, 16 skipped
This lode touches no Rust, so unit CI not building the Rust tree is expected and fine.
fix(release): tighten root launcher install proof and coverage
The install-proof root launcher derivation now enumerates every .data/scripts member before validating the exact launcher set, instead of filtering to known launcher names first. The previous shape would have accepted a root wheel carrying an unexpected extra script member from this authority, even though the candidate rail's check_dist() catches it earlier; a changed authority should not be subset-shaped.
The cold uv tool install integration test now invokes solstone --version through its uv-created path and asserts exact stdout and exit status, so both public commands are proven callable rather than only proven present.
The no-Python spy now wraps both python3 and python. Sibling-Python resolution tries python3 first and falls back to python, so wrapping only python3 would have missed a regression that spawned the fallback interpreter on a native path.
Two stale-wording corrections have no behavior change: solstone/think/call.py no longer calls the compatibility subtree a helper, since there is no helper script after the launcher move, and the recovery-path error text in release_candidate_driver.py now names the retained payload executable member it actually validates instead of a ledger native member.
Validation: focused suites all green: test_release_install_smoke.py 25 passed, test_release_install_smoke_distribution_enumeration.py 9 passed, test_release_candidate_driver.py 140 passed, test_release_publish.py 35 passed, test_release_proof_host.py 20 passed, test_channel_adapters.py 13 passed, make test-integration 33 passed. make ci was rerun because the install-proof change touches a shared cross-cutting release authority; it exited 0 on the settled tree (14918 passed, 16 skipped).
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, and nothing that builds or contacts a host. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(spp): enforce production PCR pin at confidential transport chokepoint
Add the production PCR pin registry and wire production_policy() into _establish_channel_locked, the single production establish chokepoint for confidential transport.
Preserve the new pcr_pin_mismatch reason across SNP appraisal, composite verification, RA-TLS verification, transport recorded state, and brain_cli mapping via PcrPinMismatchError caught before the generic CPU-leg handler.
Promote check_pcr_fingerprint as the shared digest-and-compare helper, remove _check_pcr_policy, and apply the same check to phase-2 exporter proof after quote signature verification.
Policy() remains record-mode by default; no wire contract, fixture, loopback script, or engine behavior changes.
fix(doctor): name the macOS journal.app / LaunchAgent supervisor conflict
journal doctor could report a coherent-looking host while journal.app and the legacy org.solpbc.solstone LaunchAgent were both supervising one journal.
Add a read-only inspect_supervisor_conflict() reader in service.py. It gathers three evidence axes: the legacy plist, the launchd label, and the journal.app process. Each axis has an explicit unknown state. journal.app is matched only among current-UID processes by executable-path suffix, so App Translocation qualifies and journal:supervisor does not.
Add a Darwin-only blocker check, supervisor_conflict, to JOURNAL_CHECKS. Unknown evidence never maps to green: proven conflict fails, any unknown axis warns and names the axis, and only fully-known non-conflict states pass.
Keep the whole doctor report coherent with a run_checks post-pass. A proven conflict replaces every other check's fix with a pointer to the single remediation, journal service uninstall. Unknown topology withholds only service lifecycle actions. Diagnoses always stay visible.
Replace the self-defeating rm ~/Library/LaunchAgents/org.solpbc.solstone.plist && journal setup guidance, which reinstalled the LaunchAgent it removed. Linux/systemd behavior is unchanged; the check skips on Linux.
The tests/systemd-test/ edits are comment-and-README-only wording changes from '&& journal setup' to ', then journal setup'. No command changed; this satisfies the repo-wide grep gate for that string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(convey): escape the notification badge
_createCard interpolated n.badge raw into innerHTML while every neighbouring field, including n.app, n.title, and n.message, was escaped, so a caller could inject markup through the badge. The value is caller-controlled end to end: sol notify --badge is free-form and websocket.js forwards raw callosum payloads into show().
_updateCard already assigned the badge with textContent, so initial render and update render disagreed. This aligns them on the safe behaviour.
Found while auditing the icon-contract change. It is the same class of defect that change closes for icon, in the same renderer, but a different field, hence a separate commit.
The test helper assertNoPayload was tightened to match event-handler markup only inside a real tag, because correctly escaped output legitimately contains the literal text onerror.
fix(indexer): match Python markdown chunk grouping
Native markdown chunking projected about 170,580 excess chunks on the 71,208-file real corpus: 1,377,411 native chunks versus 1,206,831 Python chunks. Add the generated markdown oracle fixture, the isolated differential corpus, and the native implementation changes in one commit because the green-expecting integration test would fail if committed before the behavior fix.
Port the Python structural rules into the native chunker: intro-paragraph consumption before lists and tables; definition-list grouping when at least two items match and matches are at least 50%; one chunk per top-level list item with nested and multi-block item content concatenated in document order; table row splitting with headers carried into every body-row chunk and no header-only chunk; single-chunk blockquotes; heading-only and thematic-break-only inputs producing no chunks; removal of the whole-input fallback; code-fence info strings retained; and pulldown-cmark Options::ENABLE_TABLES.
Replace raw production chunk_markdown access with one bounded format_markdown entry point at both markdown producers. That entry point performs the 2048-char markdown line sanitization, carries warnings through ProducedChunks and ScanReport, and applies the 4096-char header-stub cap.
Capture red-then-green evidence with the unchanged Jaccard >= 0.90 and mutual-top-3 gate. Before the fix, markdown_parity_single_term, markdown_parity_and, and markdown_parity_phrase were each 0.667 Jaccard and markdown_parity_prefix_code_info was 0.0; the run classified unexpected-differs with failed_components ["fulltext"], reproduced on fe07e0f58. After the fix, all five parity cases are 1.0, classification is functionally-equal, and failed_components is empty.
Delete the tautological markdown_producer_wraps_chunker_without_content_changes test and replace it with oracle-driven producer tests plus Rust oracle token assertions against the generated fixture.
Document the single accepted divergence: oversized-chunk stub size tokens are normalized in the fixture because exact parity would require byte-identical rendering, which is explicitly out of scope.
test(inventory): exclude build outputs from the repo-write inventory
The sandbox marker refusal matrix takes a before/after repository inventory for
each of nine cases. target and dist were walked, so on a working clone the walk
covered ~44k entries, core/target alone contributing ~23k files at ~7 GB, and
eighteen full walks pushed the test past pytest 15s timeout. A clean checkout of
the same commit walks ~73 MB and the same test passes in 1.75s, so the verdict
depended on how much the clone had been built in rather than on the product.
target and dist are build outputs, not repository source, and cargo and uv
rewrite them constantly. That is the same category as the .venv and cache
entries already in this set, whose name says runtime dir names.
The exclusion cannot blind the check: tests/test_repo_inventory.py parametrizes
its visibility cases over this set, so both new names automatically gain the
proofs that a regular file, symlink, or fifo with that basename stays
inventoried and that a symlink by that name is not followed, alongside the
existing proof that ordinary writes are reported. 99 inventory tests pass.
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
feat(todos): remove the todos feature in its entirety (product retreat)
Founder-approved retreat of the todos productivity feature. Deletes the app
(solstone/apps/todos/), its three cogitate talents, all eight `sol call todos`
verbs, the Flask routes, the storage/domain layer, the indexer formatter, every
external wiring that named todos, every cogitate/routine prompt that fetched or
mentioned it, the dedicated tests/fixtures, the obsolete API/visual baselines, and
the docs. No compatibility shim, no deprecation window, no read-only remnant.
Owner data is untouched: nothing under journal/ is modified — the owner's on-disk
todo JSONL stays inert.
Substrate boundary (deliberate asymmetry): the journal portability/orphan-detection
substrate that treats `facets/<facet>/todos/*.jsonl` as a persistent file-type is
LEFT INTACT — observe/export.py, apps/import/facet_ingest.py, apps/import/routes.py,
and think/facets.py `content_subdirs`. Owner todo data persists, so exported journals
must still carry it and the orphan-detector must still see todo-only facets as
content-bearing. Only think/merge.py's local `facet merge` `source_todos_dir` block
is removed, because it is feature behavior, not the import/export portability layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
fix(think): gate level:low facets out of activity creation; per-facet descriptions
ActivityStateMachine.update treated every sense facet as activity-worthy and
stamped the segment-level activity_summary onto every record. A code-signing
segment with five facets produced five activity records — two of them
peripheral (level:low) — all sharing the dominant work summary.
Two surgical changes in update():
- Exclude any facet whose raw level is exactly "low" when building
current_facets, so it is treated identically to an absent facet: it never
creates or continues an activity, and a previously-active facet that drops
to low winds down through the existing 2-segment facet-gone hysteresis
(no new/abrupt end path). Missing/invalid level still normalizes to medium
and stays engaged.
- Source each record's description from that facet's own facets[i].activity,
falling back to activity_summary only when the per-facet text is
missing/empty.
Forward-behavior only; already-written records are unchanged. level_avg,
active_entities, idle/gap/type-change handling, and completed-record shape are
untouched for high/medium facets.
Tests: new TestLevelGate covers AC1-AC5 (the founder's example facet set,
per-facet vs distinct descriptions, summary fallback, drop-to-low hysteresis,
all-low wind-down). Reconciled six low->medium fixtures whose intent was
multi-facet/pipeline behavior, and three description assertions plus a
facet_activity test-helper param so the per-facet description source is proven.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(threads): make four unit-gate concurrency tests deterministic
Four dependency-light unit tests observed genuinely-concurrent work by racing a
wall-clock deadline (fixed sleeps / poll loops). Each used a different concurrency
mechanism and gets its own explicit synchronization signal instead — with the real
concurrency two of them exist to prove left intact. Test-only; no production change.
T1 test_admission::test_two_waiters_keep_throttle_state_until_both_end
Mechanism: a threading.Semaphore released inside the test's on_throttle_start
callback, acquired twice; threads[0].join() for the mid-teardown check.
Why: wait_for_memory_headroom increments _throttle_count inside _lock before
firing on_throttle_start outside it, and _finish_wait decrements before the target
returns — so awaiting the callback twice guarantees count==2, and join guarantees
count==1, with both real OS threads still running concurrently.
Anti-vacuity: asserting count==3 failed as expected (E assert 2 == 3).
T2 test_callosum::test_client_disconnected_idle_blocks_and_drops_emit
Mechanism: kept the REAL reconnect thread; two threading.Events — one set on the
first stop_event.wait (disconnected idle block reached), one set by a scoped INFO
log handler when the drop line is emitted. Kept the 1<=wait_calls<=20 assertion.
Why: the idle-block and drop-log both live in local closures inside _run_loop and
are not externally callable, so driving them directly would require refactoring
production (out of scope); the thread is retained and only the waits made explicit.
Anti-vacuity: pointing the watcher at a never-logged string timed out the
drop_logged wait as expected.
T3 test_cortex:: dedups / fifo / duplicate / isolates
Mechanism: replaced each qsize()==0 && _pending_spawns==0 poll with
spawn_queue.join(). Real worker thread and unbounded-loop-until-stop_event shape
preserved (an inline fake would hang, since stop_event is set only post-start).
Why: the worker's finally decrements _pending_spawns then task_done(), and
_handle_request increments before put(), so join() returning implies both
qsize()==0 and _pending_spawns==0. No timeout param by design — bounded by the
suite pytest-timeout.
Anti-vacuity: asserting FIFO order reversed failed as expected.
T4 test_voice_brain::test_schedule_refresh_updates_instruction
Mechanism: wrapped brain._complete_future via monkeypatch to set a threading.Event
after the original runs (filtered to attr_name=="refresh_future"); wait on it, then
assert. Kept the future.result() tuple assertion.
Why: schedule_refresh's add_done_callback late-binds _complete_future from the
module namespace, and a Future notifies result() waiters before running done-
callbacks; wrapping the callback removes the race while preserving the real
schedule_refresh / run_coroutine_threadsafe / add_done_callback wiring.
Anti-vacuity: returning "Other voice" failed the instruction assertion as expected.
Also removed the now-dead local _wait_until helpers (test_admission.py,
test_cortex.py) and the newly-unused Callable import; swapped test_voice_brain's
now-unused `time` import for `threading`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(release): non-destructive, mirror-bound, signed-packet make audit
Replaces the GitHub-DB-refreshing make audit with a stable, non-destructive audit: verifies a minisign-signed freshness receipt against pinned sol trust (key ID + public-key SHA-256), materializes the RustSec advisory DB only from a locally-supplied git bundle at the receipt's signed commit, and runs the pinned cargo-deny advisory check fully offline against that isolated DB, emitting exactly one JSON success witness.
Four explicit operator inputs (AUDIT_ADVISORY_BUNDLE/RECEIPT/PUBKEY/LOCATOR); signature is derived as the adjacent <receipt>.minisig; locator is used only as cargo-deny's db-urls identity and never contacted; no GitHub fallback.
New authority module scripts/advisory_mirror_audit.py reuses the release-advisory isolation, minisign, and redaction primitives via import; the release-candidate prepare_policy_run path, PolicyRun/ledger schemas, and the 24h/14d rules are unchanged (regression-covered).
Recipe keeps the cargo-deny + minisign preflights, redirects preflight stdout to stderr, and suppresses command echo so make audit stdout is exactly the witness JSON and the private locator is never printed.
Adds a deterministic 113-test suite (fake tools/packets, real-git bundle materialization), rewrites the audit recipe contract test, and reconciles the now-false release_advisory_policy docstring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(convey): remove web-UI password, login, and session auth
port 5015 is loopback-only, and mtls on 7657 is the sole network transport as of 0.6.0, so the web password gated nothing. remove the login form, flask session-cookie login, http basic auth, the trust_localhost localhost bypass, the journal password cli, and the convey.secret/app.secret_key flask-session machinery.
refactor the require_login gate to require_access while keeping private link revocation auth and the first-run /init onboarding redirect. there is no migration: orphan convey.password_hash, convey.secret, and trust_localhost keys in existing journal.json files are ignored, and _NEVER_TRANSFER_PATHS still scrubs password_hash and secret on export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(backup): vendor SHA-pinned restic v0.19.0 + secret-scrubbed runner
Foundation lode for sol private backup (spb): a new solstone/think/backup/
package with two concerns and no business logic (no keys, repo-init,
backup/restore/prune, scheduling, UI, or CLI — those are later lodes).
- readiness.py (stdlib-only, read-only): pinned restic 0.19.0 constants, the
four .bz2 SHA256 pins, the official GitHub download-URL template, per-platform
tool dirs, os/arch mapping (x86_64->amd64, aarch64->arm64), sentinel load +
reuse gate (check_restic_ready), and on-disk binary verification.
- install.py (the writer): ensure_restic acquires the per-platform .bz2 from the
official restic GitHub release (never a sol-pbc URL), SHA-verifies against the
embedded pin before decompress/exec (fail-closed, no re-download loop),
decompresses, chmods +x, and atomically installs the binary, a drift-detecting
sentinel, and restic's BSD-2 LICENSE via temp-file + Path.rename. A bundled
.bz2 override (SOLSTONE_RESTIC_BUNDLE) routes through the same pin-verify path.
- runner.py (the runner, capture-mode only): run_restic passes every secret via
env (RESTIC_PASSWORD / backend AWS_*/B2_*), keeps RESTIC_REPOSITORY
credential-free, builds a minimal allowlisted child env, never uses
--insecure-tls, supports --json and --max-repack-size, and scrubs all secrets
from stdout/stderr/json/argv on success, non-zero-exit, and timeout paths.
Long-running/streaming mode is deferred to avoid leaking restic output through
ManagedProcess health logs + the callosum logs tract.
Network-mocked unit tests cover fail-closed verification, sentinel reuse,
per-platform asset selection, the bundled override, and the load-bearing runner
secret-scrubbing invariants, plus a skip-if-restic-absent local: round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(backup): cover offload restore safety regressions
Add the partial-restic-failure regression that writes one of three recorded files, rolls it back, preserves the full folded ledger state, and proves a later offload run cannot append a remnant-only event for that segment.
Tighten the acceptance traps: same-size different-digest verification, rollback only touching recorded paths, both sides of the free-space guard, restore-all summed refusal, terminal last_restore recording, default-stream rediscovery, fresh degraded ledger status under the measurement cache, and direct route/CLI restore seams.
feat(backup): spb L2 — keys, repo init, BYO destination + backup config schema
Second lode of sol private backup (spb). Adds the crypto + config layer
on top of L1's vendored restic binary and secret-scrubbed runner:
- keys.py: daily key (token_urlsafe) + 64-char Crockford recovery key
(secrets.choice per char), 16x4 display form, and parse_recovery_key —
the single canonical entry parser (folds I/L->1, O->0; the L4 contract
for restore + regeneration) plus a delegating confirm helper.
- destination.py: Destination model, credential-free RESTIC_REPOSITORY +
s3/b2 backend-env assembly, and a sanitized `restic cat config` probe
mapping returncodes to fixed owner-safe reason codes (never leaks
restic stderr).
- repo.py: idempotent two-key repo init — restic init (daily) then key
add (recovery) delivered via an os.pipe FD (never on disk, never on
argv); probe-the-repo state machine self-heals partial failures and
refuses to overwrite a repo the daily key can't unlock.
- state.py: backup config-section accessors via journal_config (0600),
per-field default-at-read for existing journals, get-or-create that
respects a hand-set key, and a secret-free status_view.
- runner.py: minimal pass_fds passthrough (default empty; byte-identical
for existing callers) enabling the pipe-FD password delivery.
- journal_default.json: pinned `backup` section for fresh journals.
Contacts no sol-pbc service. Keys/config only — backup/restore/prune
execution, scheduling, UI, and CLI verbs land in later lodes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(backup): spb L2 — keys, repo init, BYO destination + backup config schema
Second lode of sol private backup (spb). Adds the crypto + config layer
on top of L1's vendored restic binary and secret-scrubbed runner:
- keys.py: daily key (token_urlsafe) + 64-char Crockford recovery key
(secrets.choice per char), 16x4 display form, and parse_recovery_key —
the single canonical entry parser (folds I/L->1, O->0; the L4 contract
for restore + regeneration) plus a delegating confirm helper.
- destination.py: Destination model, credential-free RESTIC_REPOSITORY +
s3/b2 backend-env assembly, and a sanitized `restic cat config` probe
mapping returncodes to fixed owner-safe reason codes (never leaks
restic stderr).
- repo.py: idempotent two-key repo init — restic init (daily) then key
add (recovery) delivered via an os.pipe FD (never on disk, never on
argv); probe-the-repo state machine self-heals partial failures and
refuses to overwrite a repo the daily key can't unlock.
- state.py: backup config-section accessors via journal_config (0600),
per-field default-at-read for existing journals, get-or-create that
respects a hand-set key, and a secret-free status_view.
- runner.py: minimal pass_fds passthrough (default empty; byte-identical
for existing callers) enabling the pipe-FD password delivery.
- journal_default.json: pinned `backup` section for fresh journals.
Contacts no sol-pbc service. Keys/config only — backup/restore/prune
execution, scheduling, UI, and CLI verbs land in later lodes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(backup): cover media offload restore
Cover restore engine behavior for daily-key restic calls, _default stream layout, operated append-only sessions, BYO hosted seams, free-space refusal, no-op folding, partial restore sweeps, rollback, and callosum staying untouched.
Pin the real restic contract that drove the design: restic restore <snapshot_id>:<segment_dir> --target <segment_dir> --include /<name> may exit 0 while restoring zero files for a missing include, so tests assert missing files are caught by existence, size, and sha256 verification.
Also cover the exact five offload routes, the three journal backup offload CLI verbs, last_restore state, device_total_bytes(), and resolve_segment_dir() default-stream behavior.
feat(backup): spb L4 — restore, teardown & recovery-key rotation
Add the L4 sol private backup engine hooks: restore_journal(),
teardown_backup(), and rotate_recovery_key(). Restore uses restic's snapshot
subpath form (`restore latest:<path> --target <journal>`), reuses
parse_recovery_key() folding, persists destination + recovery key + confirmed
only after a successful restore, runs a non-fatal post-restore restic check,
and triggers deterministic scan_journal(full=True) reindexing.
Teardown enumerates snapshots and runs `forget <ids> --prune`, because
`--unsafe-allow-remove-all` is rejected without a filter, then clears local
backup config only after remote-delete success. Rotation follows
add -> confirm -> verify -> remove-old, so there is never a zero-recovery-key
window, and leaves confirmed_recovery_key false for the owner to re-save the
new key.
Move shared reason_for_returncode() and select_summary() to runner.py. Add
state.py setters set_recovery_key() and clear_backup_config(). Add repo.py
ResticKeyError plus _capture_current_key_id() and _remove_key(). Keep secrets
env-only and out of logs/results, with the new recovery key returned only from
successful rotation. Add mocked coverage plus a real local-restic round-trip
integration test. Engine hooks only; no UI, CLI, or owner-facing copy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(spp): enforce production PCR pin at confidential transport chokepoint
Add the production PCR pin registry and wire production_policy() into _establish_channel_locked, the single production establish chokepoint for confidential transport.
Preserve the new pcr_pin_mismatch reason across SNP appraisal, composite verification, RA-TLS verification, transport recorded state, and brain_cli mapping via PcrPinMismatchError caught before the generic CPU-leg handler.
Promote check_pcr_fingerprint as the shared digest-and-compare helper, remove _check_pcr_policy, and apply the same check to phase-2 exporter proof after quote signature verification.
Policy() remains record-mode by default; no wire contract, fixture, loopback script, or engine behavior changes.
feat(browser): treat {host}.browser as a first-class journal modality
Make the journal validate, render, and index the solstone-browser
extension's per-site browser_<host>.jsonl uploads, and unblock
browser-only segments in the pipeline.
- Contract: add browser.schema.json (format_id browser-jsonl,
file_kind headered_jsonl) with an identical header/record row union
(required floor t/ts only, ctx optional, additionalProperties true)
so multi-ctx, legacy ctx-less, and delta-first files all validate;
route browser_*.jsonl in schema_for_filename() so ingest gates it.
- Formatter: add solstone/think/browser_formatter.py — format_browser()
(one snapshot chunk per segment_start, add/update delta chunks,
remove skipped, epoch-ms timestamps, agent="browser") registered
indexed=True, plus format_browser_text(). Output is bounded per file
to the shared 32k extraction budget with an explicit truncation marker.
- Visibility (atomic): detect browser data-state (analyzed on presence),
add "browser" to the scan/cluster modality set, and add "browser" to
SEGMENT_NO_PROCESSING_MODALITIES so browser-only segments are visible
and yield zero completion blockers.
- Consistency: browser_*.jsonl counts as segment content in the verify
CLI; layout.json accepts the browser producer path; bundle regenerated.
- Fixtures + tests cover formatter shape, degenerate/oversized cases,
contract validation, data-state/scan visibility, zero-blocker
completion, and stream/agent-tagged indexing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
refactor(routines): excise owner-facing routines feature
Remove the owner-facing routines feature entirely: the routine talent,
the scheduler engine (think/routines.py), the journal routines CLI, the
9 templates + SKILL, the suggestion engine (_routine_context), and every
home-pulse / chat / exec surfacing. No replacement, no shims, no
owner-data migration — journal/routines/ is left inert on disk.
Tighten the cogitate journal-command allowlist 4->3 ({identity, health,
talent}) across all three synced definitions. Surgically clean the kept
consumer talents so no live run names `journal routines`. Regenerate the
three affected API baselines. The MaintenanceRoutine system, coroutine
call sites, and English-word prose are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(supervisor): harden provider runtime ownership
Write a provider's durable ownership record before publishing its port file, and fail closed when that ownership write fails. The child is cleaned, no port is published, and startup does not mark the provider ready. Previously a port file could point at a live process with no ownership record, leaving later cleanup unable to prove generation ownership.
Route every launch and stop cleanup path through the preserving cleanup wrapper. If termination raises, the managed handle is adopted by the cleanup-failed reconciler instead of being dropped and orphaning the child.
Restore Parakeet install bootstrap reachability after the Callosum start commands were removed. Missing artifacts now trigger bootstrap from reconciliation only when the desired-state table allows acquisition, with install and launch still communicating through durable Lode B fact changes.
Also preserve the Parakeet STT admission latch across ready and probe writes, move local context-window and Parakeet placement writes behind the generation fence, tighten the provider start-command gate for from solstone.think import callosum, rename the sweepable provider proctitle set, and delete stale helpers.
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(think): synthesis access-tier — sol-surface-only cogitate (no raw-fs reads)
Add a fourth locked cogitate access tier `synthesis`
(sol=True, reads=False, submit=False): the journal is reached only through
`sol` domain commands, with no raw-filesystem read tier (read_file /
list_directory / glob / grep_search) and no outbound submit. It fills the last
meaningful corner of the (reads, submit) capability space — `outbound` was the
only reads=False tier but it drags submit=True along, which is wrong for a pure
synthesis talent.
This is the mechanism half of the weekly_reflection fs-fallback fix; 23d6498a
shipped the prose ("gather only through sol; do not walk the raw tree"), and a
2026-06-17 weekly run still logged 12 fs-fallback + 4 repeat-flail of 29 actions
because the read tools were merely discouraged, not withheld. The tier removes
them at registration, so the constraint the prose asks for is now enforced.
Set weekly_reflection + partner to `synthesis`. weekly_reflection needed no
body change (prose already forbids raw reads). partner had a live Step-1
`read_file identity/partner.md` dependency, swapped to `journal identity
partner` (the settled sol-surface read form, policy-allowed at reads=False).
The locked-contract RuntimeError guards keep COGITATE_ACCESS_TIERS in sync with
the capability map; all caps/display/validation consumers are contract-driven
and auto-adapt (openhands tool registration, cogitate_policy, talent_cli
inventory + tier audit). Updated the contract vocabulary-lock test, added the
capability row, made the finalization-harness tool-surface assertion tier-aware
(weekly_reflection is now sol-only), and documented the tier in docs/COGITATE.md.
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
feat(push): self-provision reach relay token for push dispatch
Teach the journal to self-provision its push relay credential from the
hosted reach service using its durable link identity (home:{instance_id}
+ local CA), replacing the read-only PUSH_RELAY_SECRET env fallback and
the services.push.relay_token config string (clean break, no shim).
- link/ca.py: add mint_reach_assertion() — ES256 home-reach JWT
(aud=solstone-reach, scope=push.relay.enroll, 240s, no device_fp)
- think/push/reach.py: read_reach_token() pure reader +
ensure_reach_token() enroll/refresh client; POST
/reach/push/relay-token; structured services.push.reach_token state
(token/instance_id/expires_at/expires_epoch) written via
journal_config under hold_config_lock, lock held only around the RMW;
refresh on absent/expired/malformed/instance-mismatch/within-margin,
1h margin; never drops a still-valid token on a failed refresh
- portal_dispatch: devices-first, then ensure_reach_token(); send
Authorization: Bearer; env-normalize/pruning/short-circuit unchanged
- convey/push.py: register best-effort-enrolls; /status uses the pure
reader; /test self-heals via dispatch
- triggers: drop the relay-token pre-checks (token failures collapse to
portal_unavailable); never raise
- delete relay_auth.py; remove PUSH_RELAY_SECRET + relay_token paths
- link/paths.py: make LinkState.load() a truly pure read (no link/ dir
creation as a path-resolution side effect)
- secret hygiene: only status codes + exception type names are logged
- update docs/design/push.md (reach enrollment, state shape, wire
contract, corrected nudge reasons)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(chat): owner-language talent labels and FIFO message queue
T1.3 — owner-language talent labels
Replace the three raw `{{ ev.name }} started|finished|errored` literals
across all owner-facing surfaces (SSR _chat_event.html, chat-bar
renderTalentChip/handleChatEvent in app.html, chat-workspace JS in
workspace.html) with six locked constants in a new module
`solstone/apps/chat/copy.py`, mirrored to `static/chat_copy.js` with a
parity test (mirrors the chat_reasons.py / chat_reasons.js pattern).
A new helper `talent_label_for(target, status)` raises ValueError on
unknown values, matching the existing `_parse_chat_result` enum
enforcement at chat.py:1067.
The `task` field on each talent card remains verbatim model output.
Legacy test fixtures in `solstone/apps/chat/tests/test_routes.py`
used `"search"` as a talent name; that target was never schema-valid
(`talent_request.target` enum is `{exec, reflection}` per
`solstone/talent/chat.schema.json`). Renamed to schema-valid `"exec"`.
T1.4 — FIFO chat queue with depth indicator
Replace the single-slot `_queued_trigger: dict | None` with a FIFO
`deque` (`_queued_triggers`). Every owner message gets its own queued
slot — the prior silent-overwrite bug at depth>=2 is gone.
Cap depth at 10: the 11th POST at full capacity returns HTTP 429
`chat_queue_full` before any `owner_message` event is appended, the
textarea content is retained client-side, and the chat-bar shows the
locked `CHAT_QUEUE_DEPTH_CAP_MESSAGE`.
Depth signal channel: a new callosum chat-stream event
`chat_queue_depth` is emitted on every enqueue and dequeue (under the
existing `_state_lock`; `append_chat_event` does not re-enter the
lock). Chosen over a session-poll/client-counter because it matches
the existing chat-stream fan-out pattern and survives multi-tab.
`reduce_chat_state` exposes `queue_depth` for hydrate, which made
`tests/baselines/api/chat/session.json` gain a `queue_depth: 0` field.
Tests cover FIFO ordering, depth-cap rejection, error-on-N-doesn't-
abort-N+1, label render mapping, and Python/JS copy parity.
refactor(convey): fold shared chrome copy casing and ellipses
Fold manifest-touched shared Convey action, report, diagnostic, status, date-nav, day-grid, and shell fallback copy. Keep the Python copy constants and browser mirrors aligned for the approved CONVEY_ACTION_* and CONVEY_REPORT_* rows.
Jer override: the absent 'What the journal said:' row is a no-op, so Server reason: deliberately remains unchanged in convey/copy.py, convey_copy.js, app.js, and diagnostic-console.js.
fix(convey): stop mobile facet pills shrinking so the row can scroll
On mobile, the facet-pill row squeezed pills to fit instead of scrolling.
The prior fix 09d1436c9 made .facet-bar .facet-pills-container compute overflow-x: auto under the 768px breakpoint, but .facet-pill still carried flex-shrink: 1. The pills compressed to fit, so the row never overflowed and overflow-x: auto had nothing to scroll.
Under the same breakpoint, .facet-bar .facet-pills-container .facet-pill { flex-shrink: 0 } keeps pills at content width, and justify-content: flex-start on the container makes the row scroll from the first pill.
The cascade mechanisms differ by rule. flex-shrink: 0 wins by specificity: (0,3,0) vs the unconditional .facet-pill at (0,1,0); source order runs against it because the media block is earlier in the file. justify-content: flex-start wins by source order: it has equal (0,2,0) specificity to the base rule and is placed after it.
Base rules are overridden, never edited in place. .facet-pill .label needs no override once the parent pill stops shrinking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(convey): make the notification icon a Lucide name
The notification icon contract is now a Lucide icon name, not an emoji. Emitters in JS, Python, and sol notify help send names; the renderer resolves names to vouched SVG and never renders the caller's value as markup. A legacy glyph-to-name table keeps already-persisted localStorage['solstone:notification_history'] entries rendering correctly; that is a migration path for external data, not a backwards-compatibility shim.
The honest price is that this route deliberately manufactures window.ConveyIcons.svg('name') literals inside the notifications object whose only consumer is the guard test's allow_list <= used direction. It trades a one-line regex change for a hand-maintained duplicate list that must stay in sync with CONVEY_ICON_NAMES, and it narrows the notification namespace from 36 available names to 13. It is the right call here because the safety property - a caller can never inject markup through icon - is worth it. It is not parity with L3's genuinely-free zero-mechanism route, and a later session must not read it as precedent for always manufacturing literals.
D5 and D6 were fixed, not deferred. ERROR_ICON in SurfaceState was a hand-rolled inline triangle-alert SVG; it now uses window.ConveyIcons.svg('triangle-alert'). _updateCard never refreshed the icon even though show() updates existing.icon for keyed notifications - invisible while both were the same emoji, but a stale icon under resolved SVG. It now refreshes from the resolver.
The browser Notification icon member was dropped because it is specified as a URL; the old emoji was parsed as a relative URL and fetched, which always failed. Title, body, and tag still deliver.
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(core): add solstone-core wheel rail
Add a maturin solstone-core leaf and wire the release rail for static musl Linux wheels on x86_64 and aarch64, with explicit manylinux2014 tags. Maturin audits the built binary's actual requirements, so the fully static helper honestly earns the low floor while staying small; the aarch64 leg cross-links with rust-lld and no external toolchain.
Add the supervisor startup handshake before flock acquisition or any journal mutation. Packaged installs on covered platforms require solstone-core dist metadata and the env-local helper binary to agree, exiting 78 on skew; source checkouts without the helper dist continue with a visible skip.
Keep version lockstep and artifact checks strict across render_packaging, extras consistency, wheel contents, release.sh, and the macOS repack path. Add tests for render drift, covered-platform marker agreement, wheel contents, local wheel install, preflight, and real supervisor wiring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(speakers): harden identify ledger remediation
Fold existing identify operations before planning so retried request ids replay or conflict instead of returning stale planning errors, and resume uses the stored prepared plan. Add the folded undoing state so interrupted undo work is never replayed as the original success.
Make the retro tracker phase fail loudly on voiceprint save errors so incomplete work is not checkpointed. Emit durable undo_repair_required records for structural undo failures, and restore the stable response contract fields retro_voiceprints_saved, operation_state, and corrections_appended.
Map recoverable, repair, conflict, not-found, in-progress, and undoing outcomes to non-2xx route envelopes. Keep store read paths write-free, extend dismissal filtering to the speakers status cluster surface, fail closed on absent or unreadable edge state while scanning edge sources, and resurface keep-separate curation rows once the assertion is tombstoned or the watermark is exceeded.
Remove the dead resolved-cluster writer, correct the lock-order docs, and reject duplicate reviewed near-match ids explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(think): rebuild pulse as a lite generate on cadence + retire awareness_tender
Rebuild pulse from a cogitate/schedule:segment tool-loop into a lite
generate/schedule:cadence talent — the structural twin of steward. Pulse is
now a standing, always-current artifact persisted to the day-jsonl accumulator
(chronicle/<day>/talents/pulse.jsonl), serving the home glance and chat
situational awareness (incl. mobile cold-open).
- New accumulate:true frontmatter flag: _apply_output_persistence skips the
single-file write so a generate talent persists only via its post-hook's
append_record (output:json + schema stay declared; the JSON schema still
reaches the model). Reusable for steward's future migration; covers the
schedules that route through _apply_output_persistence (cadence + daily/weekly).
- pulse.{md,py,schema.json}: closed 4-field schema (title, one_sentence,
full_details, needs_you); pre-hook gathers context in-process (previous
record, completed-since window, awareness/anticipated/entities/partner —
folding in the deleted awareness_tender's deterministic reads) and degrades
gaps without crashing; post-hook normalizes to the closed contract and
appends the record.
- Readers migrated onto read_latest: home (strict today-gate, lookback_days=0),
chat ($situational, default lookback for cold-open), morning briefing.
- Fix _load_flow_md: resolve via day_path(today) (was missing chronicle/);
documented that flow.md is no longer produced by any talent.
- Delete awareness_tender, the journal identity pulse --write / identity
awareness verbs, the awareness.md identity-template entry, the pulse/
awareness_tender segment-dispatch blocks, NEVER_SKIP_DAILY, and the now-
vestigial recommend.pulse_update (sense.md + sense.schema.json + consumers).
- Tests/baselines/docs updated to the new inventory; new coverage for the
pulse hooks, the home/chat readers, the accumulate no-single-file guarantee,
and the agent=pulse search round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(describe): scene-cut + stride frame winnowing; deterministic extraction
Add a content-aware stage on top of the existing dHash frame qualification in
VideoProcessor.process(): qualified frames at Hamming distance >= 25 are tagged
scene cuts and always kept; non-scene-cut qualified frames arriving < 5s after
the last KEPT frame are stride-dropped. All three gates (dHash change, scene
cut, stride floor) measure against the single last-kept reference, which
advances only on a kept frame. Thresholds are config-backed under `describe`
(scene_cut_threshold=25, min_stride_seconds=5.0) and honored at both process()
call sites via VideoProcessor.__init__. Per-segment winnowing metrics
(raw/dhash_qualified/scene_cut/stride_dropped/kept) emit as one INFO line; no
behavior is gated on them. The dHash body, metadata header, JSONL schema, and
per-sensor change-detection are unchanged.
extract.py: importance is now a hard per-category filter (ignore->0, low-><=2
per category, normal/high uncapped) via _apply_category_caps, applied before the
first-frame guarantee. The AI-fallback selector is now deterministic greedy
max-temporal-spread (seeded lowest-frame_id, lowest-frame_id tie-break) instead
of random.sample; the `random` import and stale "advisory" wording are gone.
Reimplementation on top of main (not a merge of the ~198-commit-old PR branch).
The MobileViT embedding stage is deliberately NOT ported: three local evals
proved it inert on screencasts, and it does not earn its complexity or 21 MB
wheel weight. No new runtime dependency, no new assets.
Design decisions:
- Config keys `describe.scene_cut_threshold` (25) / `describe.min_stride_seconds`
(5.0); module constants SCENE_CUT_THRESHOLD / MIN_STRIDE_SECONDS are the
defaults and the values tests import. No config for DHASH_THRESHOLD.
- Per-frame keep decision is a pure module-level _winnow_decision(); process()
owns reference and counter state.
- Counters: raw = every decoder-yielded frame (incl. pts=None and masked skips);
dhash_qualified = first frame + dHash-8 gate passers (== kept + stride_dropped);
scene_cut subset of kept. Identities raw >= dhash_qualified >= kept,
kept == dhash_qualified - stride_dropped, scene_cut <= kept hold in all cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(schemas): wrap 4 Class-B root-array schemas + permanent consumer unwrap (req_bfbdbux6 Lode 2/4)
WHAT: Wrap extract, detect_transcript_segment, speaker_attribution, and schedule root arrays in strict-portable one-key objects (frame_ids, segments, attributions, events). Apply the strict-portable transform at every nesting depth: strip banned schema keywords, require all object properties, set additionalProperties:false, and preserve enums, patterns, and nullable unions.
CONSUMERS: Update the four single parse-sites to unwrap the wrapper key while permanently tolerating bare-list outputs as a defensive shape-compat shim. Relocate the detect_transcript_segment schema $comment to the _SEGMENT_SCHEMA load-site code comment.
GUARDS: Promote the four Class-B ids out of PENDING_PORTABILITY. Rename the provider parity list/test generically and extend it to 17 schemas x 3 providers = 51 cases. Reconcile schema-shape and parse-site tests, including dropping the three stripped-constraint negatives by design.
SHIP NOTE: Consumer-completeness sweep found 4 single boundaries and no additional raw-list parse sites. No live path was found by which a stale cached bare-list output file is fed into schedule.post_process() or speaker_attribution.post_process(); the permanent bare-list tolerance is a defensive compatibility shim mandated by scope, not justified by a discoverable stale-cache replay path.
CI: make ci green (4922 passed, 11 skipped, 8 deselected, 1 unrelated warning).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(doctor): recognize the app-owned sol and journal launchers as healthy
install_guard._RE_MARKER accepts only numeric managed-wrapper versions 1-7, so the regular file the macOS app writes at ~/.local/bin/{sol,journal} fell through parse_wrapper to AliasState.FOREIGN. Every correct app install therefore got a permanent, false stale_alias_symlink warning telling the owner to run journal setup, which would replace a healthy launcher.
Add a narrow doctor-health recognizer, install_guard.is_app_owned_child_launcher, that requires exact equality against the rendered canonical app-owned-child body naming the literal dirname(sys.executable)/<binary> target, never resolved, plus that target being present and executable. Wire it into doctor.stale_alias_symlink_check only.
Mutation ownership is unchanged. check_alias, check_alias_detail, cmd_check, cmd_install, cmd_uninstall, provision_wrappers, and setup's wrapper/clean-uninstall steps still treat the file as FOREIGN; no AliasState member was added and import_install_guard's 2-tuple signature is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Route mutation lanes through entity resolution boundary
Move the audited journal-writing name-resolution lanes onto record_entity_resolution so tier 5-8 matches persist ambiguity before any domain write and then reuse each lane's existing unresolved representation.
This prevents low-confidence names from creating duplicates, attaching speaker labels or voiceprints, or firing irreversible identity merges. In particular merge.py now stages ambiguous source entities, speakers.merge_names refuses before merge_entity(commit=True), and discovery exits before entity creation, voiceprint saves, labels, resolved-cluster writes, and candidate retroactive confirmation.
The import bridge checks resolved choices before the identical-content shortcut, so a staged low-confidence entity can be resolved and then applied on re-ingest without leaving received, id_map, or staging state incoherent.
refactor(entities): two-phase owner-routed merge commit (write-before-delete, resumable, fail-loud)
Harden merge_entity's commit path so multi-file writes are per-file
durable, routed through the speaker-store owners, applied
write-new-before-delete-old with all destructive deletes last,
resumable via the existing merged_into marker, and fail-loud with a
machine-readable phase on partial failure.
- Route segment rewrites through attribution.py owners: labels via the
existing update_speaker_labels transform seam; corrections via a new
merge-only locked replace-all (remap_speaker_corrections_for_entity_merge)
that reads fresh under lock and remaps source_id->target_id, so a
concurrent append is preserved instead of clobbered by stale planned bytes.
- Split the commit into phase 1 (additive: identity, voiceprints, facet
relationships into target, segment rewrites) and phase 2 (destructive:
source rel dirs, discovery cache, source entity dir). The facet "move"
op is re-expressed as owner-writes-into-target + phase-2 source rmtree
(no rename, no interleaved delete).
- On a mid-commit exception, return error + failed_phase + recovery
guidance + source/target ids (source still marked merged_into) instead
of a bare error string; log loudly.
Corrections store stays byte-identical (no trailing newline); labels now
go through the owner's write_json (one-time trailing-newline
normalization). Adds race, write-before-delete, fail-loud, and byte-pin
tests; planner and dry-run result shape unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
entities: rebuild Entity Reviewer as deterministic pre-hook + single-shot generate
Convert entities:entities_review from an agentic cogitate talent (drove
`sol call entities {list,attach,aka,record-merge-candidate}` in a loop) into a
deterministic pre-hook + single-shot `generate` + deterministic post-hook,
mirroring entities:detection.
- pre_process: deterministically aggregates the facet's detected store over the
7-day window [D-7..D-1], applies fixed per-type day-count thresholds
(Person 2 / Company 3 / Project 3 / Tool 5 / other 5), type-consistency, and
not-already-attached (high-confidence match) checks, and emits eligible
candidates + variant-pair hints + prior merge decisions as a plain packet;
returns a skip_reason (no model run, no sidecar) when nothing is actionable.
- post_process: re-derives the authoritative eligible set, defensively parses the
model JSON, and applies promotions (attach_or_reactivate_entity), aliases
(add_entity_aka), and hinted merges (record_merge_candidate) through the
existing L2 owner functions — idempotently, dropping bad/ineligible rows,
never raising — and always writes a facets/<facet>/entities/<day>_review_outcome.json
observability sidecar on the non-skip path.
- The model supplies only judgment: timeless descriptions, alias/merge-direction
proposals, and the promote/decline boolean. Thresholds, type, day-count, and
eligibility are code-authoritative.
Regenerates the four talent-listing API baselines (cogitate->generate) and
updates the schedule-gated-cogitate guard in test_openhands_provider.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor(convey): remove web-UI password, login, and session auth
port 5015 is loopback-only, and mtls on 7657 is the sole network transport as of 0.6.0, so the web password gated nothing. remove the login form, flask session-cookie login, http basic auth, the trust_localhost localhost bypass, the journal password cli, and the convey.secret/app.secret_key flask-session machinery.
refactor the require_login gate to require_access while keeping private link revocation auth and the first-run /init onboarding redirect. there is no migration: orphan convey.password_hash, convey.secret, and trust_localhost keys in existing journal.json files are ignored, and _NEVER_TRANSFER_PATHS still scrubs password_hash and secret on export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(convey): remove web-UI password, login, and session auth
port 5015 is loopback-only, and mtls on 7657 is the sole network transport as of 0.6.0, so the web password gated nothing. remove the login form, flask session-cookie login, http basic auth, the trust_localhost localhost bypass, the journal password cli, and the convey.secret/app.secret_key flask-session machinery.
refactor the require_login gate to require_access while keeping private link revocation auth and the first-run /init onboarding redirect. there is no migration: orphan convey.password_hash, convey.secret, and trust_localhost keys in existing journal.json files are ignored, and _NEVER_TRANSFER_PATHS still scrubs password_hash and secret on export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(describe): scene-cut + stride frame winnowing; deterministic extraction
Add a content-aware stage on top of the existing dHash frame qualification in
VideoProcessor.process(): qualified frames at Hamming distance >= 25 are tagged
scene cuts and always kept; non-scene-cut qualified frames arriving < 5s after
the last KEPT frame are stride-dropped. All three gates (dHash change, scene
cut, stride floor) measure against the single last-kept reference, which
advances only on a kept frame. Thresholds are config-backed under `describe`
(scene_cut_threshold=25, min_stride_seconds=5.0) and honored at both process()
call sites via VideoProcessor.__init__. Per-segment winnowing metrics
(raw/dhash_qualified/scene_cut/stride_dropped/kept) emit as one INFO line; no
behavior is gated on them. The dHash body, metadata header, JSONL schema, and
per-sensor change-detection are unchanged.
extract.py: importance is now a hard per-category filter (ignore->0, low-><=2
per category, normal/high uncapped) via _apply_category_caps, applied before the
first-frame guarantee. The AI-fallback selector is now deterministic greedy
max-temporal-spread (seeded lowest-frame_id, lowest-frame_id tie-break) instead
of random.sample; the `random` import and stale "advisory" wording are gone.
Reimplementation on top of main (not a merge of the ~198-commit-old PR branch).
The MobileViT embedding stage is deliberately NOT ported: three local evals
proved it inert on screencasts, and it does not earn its complexity or 21 MB
wheel weight. No new runtime dependency, no new assets.
Design decisions:
- Config keys `describe.scene_cut_threshold` (25) / `describe.min_stride_seconds`
(5.0); module constants SCENE_CUT_THRESHOLD / MIN_STRIDE_SECONDS are the
defaults and the values tests import. No config for DHASH_THRESHOLD.
- Per-frame keep decision is a pure module-level _winnow_decision(); process()
owns reference and counter state.
- Counters: raw = every decoder-yielded frame (incl. pts=None and masked skips);
dhash_qualified = first frame + dHash-8 gate passers (== kept + stride_dropped);
scene_cut subset of kept. Identities raw >= dhash_qualified >= kept,
kept == dhash_qualified - stride_dropped, scene_cut <= kept hold in all cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(describe): scene-cut + stride frame winnowing; deterministic extraction
Add a content-aware stage on top of the existing dHash frame qualification in
VideoProcessor.process(): qualified frames at Hamming distance >= 25 are tagged
scene cuts and always kept; non-scene-cut qualified frames arriving < 5s after
the last KEPT frame are stride-dropped. All three gates (dHash change, scene
cut, stride floor) measure against the single last-kept reference, which
advances only on a kept frame. Thresholds are config-backed under `describe`
(scene_cut_threshold=25, min_stride_seconds=5.0) and honored at both process()
call sites via VideoProcessor.__init__. Per-segment winnowing metrics
(raw/dhash_qualified/scene_cut/stride_dropped/kept) emit as one INFO line; no
behavior is gated on them. The dHash body, metadata header, JSONL schema, and
per-sensor change-detection are unchanged.
extract.py: importance is now a hard per-category filter (ignore->0, low-><=2
per category, normal/high uncapped) via _apply_category_caps, applied before the
first-frame guarantee. The AI-fallback selector is now deterministic greedy
max-temporal-spread (seeded lowest-frame_id, lowest-frame_id tie-break) instead
of random.sample; the `random` import and stale "advisory" wording are gone.
Reimplementation on top of main (not a merge of the ~198-commit-old PR branch).
The MobileViT embedding stage is deliberately NOT ported: three local evals
proved it inert on screencasts, and it does not earn its complexity or 21 MB
wheel weight. No new runtime dependency, no new assets.
Design decisions:
- Config keys `describe.scene_cut_threshold` (25) / `describe.min_stride_seconds`
(5.0); module constants SCENE_CUT_THRESHOLD / MIN_STRIDE_SECONDS are the
defaults and the values tests import. No config for DHASH_THRESHOLD.
- Per-frame keep decision is a pure module-level _winnow_decision(); process()
owns reference and counter state.
- Counters: raw = every decoder-yielded frame (incl. pts=None and masked skips);
dhash_qualified = first frame + dHash-8 gate passers (== kept + stride_dropped);
scene_cut subset of kept. Identities raw >= dhash_qualified >= kept,
kept == dhash_qualified - stride_dropped, scene_cut <= kept hold in all cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(cogitate): execute sol tool as parsed argv, not bash -lc
Harden the cogitate `sol` command tool so an approved call no longer runs
through a general-purpose shell. The model-facing surface stays a command
string, but the runtime now parses it as exactly one command-line
invocation, classifies policy from the parsed argv (not regex over the raw
string), rejects shell composition (pipes, redirects, chaining, command
substitution, wrappers) on every tier, and spawns the target executable
directly as argv — resolved against the running interpreter's bin dir, no
login shell. Invalid/shell-composed commands become the existing recoverable
tool-denial observation and never consume read-call budget.
- cogitate_policy: single quote-aware classify_command returning CommandDecision
(allowed, reason, argv); deletes the raw-string regex/shell-control helpers.
- openhands: SolExecutor runs decision.argv via _run_command (subprocess, no
shell); command_not_found now reports the real missing executable.
- check_cogitate_prompts: flags shell-composed/substituted/wrapped command
spans (new shell-composition kind); lint mirrors the runtime scanner but
tolerates per-line-split multiline quoted examples.
- contract preamble + docs/COGITATE.md: a single parsed command-line
invocation, not an arbitrary shell.
- facet_newsletter: migrate off its `echo ... | sol call journal news --write`
pipe to emit_final(content=...) persisted by a new post_process hook
(facet_news), keeping the no-shell invariant clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(todos): remove the todos feature in its entirety (product retreat)
Founder-approved retreat of the todos productivity feature. Deletes the app
(solstone/apps/todos/), its three cogitate talents, all eight `sol call todos`
verbs, the Flask routes, the storage/domain layer, the indexer formatter, every
external wiring that named todos, every cogitate/routine prompt that fetched or
mentioned it, the dedicated tests/fixtures, the obsolete API/visual baselines, and
the docs. No compatibility shim, no deprecation window, no read-only remnant.
Owner data is untouched: nothing under journal/ is modified — the owner's on-disk
todo JSONL stays inert.
Substrate boundary (deliberate asymmetry): the journal portability/orphan-detection
substrate that treats `facets/<facet>/todos/*.jsonl` as a persistent file-type is
LEFT INTACT — observe/export.py, apps/import/facet_ingest.py, apps/import/routes.py,
and think/facets.py `content_subdirs`. Owner todo data persists, so exported journals
must still carry it and the orphan-detector must still see todo-only facets as
content-bearing. Only think/merge.py's local `facet merge` `source_todos_dir` block
is removed, because it is feature behavior, not the import/export portability layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(convey): optional per-facet Lucide icon override + icon picker
- Add an optional, additive `icon` (Lucide name) to facets that overrides the emoji-derived icon. The stored `emoji` stays the source of truth and the fallback; `icon` never mutates `emoji`.
- icons.py becomes the single owner of icon precedence + search: resolve_facet_icon_svg (explicit icon → emoji translation → none, degrading a dangling icon to its emoji), is_lucide_icon, and search_lucide_icons (name-first then tag matches over the vendored name+tags data).
- facets.py persists `icon` only when set; clearing removes the key (clearing an unset icon is a no-op — no change-diff, no audit, no empty key); unknown icons raise a resolution-guiding ValueError.
- Settings routes accept/validate/persist/return `icon` on create (POST) and update (PUT, now mapping ValueError→400), expose raw `icon`+resolved `icon_svg` in the facet record, and add a server-backed GET /app/settings/api/icons search endpoint (no ~1MB icon JSON shipped to the browser).
- CLI gains `--icon` on `facet create`/`facet update` (empty clears); cli.md documents it.
- Facet appearance settings get an emoji-picker-style Lucide picker modal: server-backed debounced search, inline-SVG grid in the facet color, live preview, apply / use-emoji-instead, error+retry degraded state, and a focus-trap that stays valid across grid re-renders with roving arrow-key grid nav.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(providers): install CUDA llama-server from pinned tarballs
Replace the owner-side OCI image pull for the CUDA llama-server with per-platform sha256-pinned tarballs hosted on updates.solstone.app. The CUDA lane now follows the same download, sha256 verify, safe extract, required-files check, manifest write, and atomic publish machinery as the Vulkan lane.
Key CUDA installs by the tarball sha256, write a standard llama-server-cuda provider manifest, and remove legacy OCI digest-keyed trees only after a successful replacement publish. Fit reports now account for the pinned CUDA tarball size as a known artifact.
Tests cover pin coverage, pin-present resolver selection, no-pin Vulkan fallback, sha256 fail-closed, required-file failures, traversal rejection, manifest inventory, cohort convergence, and legacy-tree cleanup.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(browser): treat {host}.browser as a first-class journal modality
Make the journal validate, render, and index the solstone-browser
extension's per-site browser_<host>.jsonl uploads, and unblock
browser-only segments in the pipeline.
- Contract: add browser.schema.json (format_id browser-jsonl,
file_kind headered_jsonl) with an identical header/record row union
(required floor t/ts only, ctx optional, additionalProperties true)
so multi-ctx, legacy ctx-less, and delta-first files all validate;
route browser_*.jsonl in schema_for_filename() so ingest gates it.
- Formatter: add solstone/think/browser_formatter.py — format_browser()
(one snapshot chunk per segment_start, add/update delta chunks,
remove skipped, epoch-ms timestamps, agent="browser") registered
indexed=True, plus format_browser_text(). Output is bounded per file
to the shared 32k extraction budget with an explicit truncation marker.
- Visibility (atomic): detect browser data-state (analyzed on presence),
add "browser" to the scan/cluster modality set, and add "browser" to
SEGMENT_NO_PROCESSING_MODALITIES so browser-only segments are visible
and yield zero completion blockers.
- Consistency: browser_*.jsonl counts as segment content in the verify
CLI; layout.json accepts the browser producer path; bundle regenerated.
- Fixtures + tests cover formatter shape, degenerate/oversized cases,
contract validation, data-state/scan visibility, zero-blocker
completion, and stream/agent-tagged indexing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor(steward): demote health pass to report-only; fix stale-marker reprocess deadlock
The deterministic steward health pass no longer auto-fires per-segment
reprocesses. `run_recipe_pass` becomes report-only (zero POSTs, zero
recipe.outcome rows; keeps its three empty-list keys for the heartbeat
pass event). Genuinely-pending segments are already re-run by the daily
sensing pre-phase, so firing remediated nothing the pipeline doesn't
handle — it only widened latency, at the cost of escalation/re-verify
bookkeeping that existed solely to track fired repairs.
Removed: the fire/detect/re-verify/escalation machinery and every
escalated_targets render branch; the unused for_range health_report
gather; the always-empty `## Trends (last 7d)` section (constant,
renderer, validator, bootstrap default, fixture); and the unmapped
reprocess_stale suggested-action (Python, talent prompt, JSON schema).
Fixed the now-sole manual reprocess path's deadlock: a stale
`.analyzing_<modality>` marker classified FAILED was never cleared, so
create_analyzing_marker hit FileExistsError and never respawned. The
FAILED branch now calls repair_modality_markers (reusing
ANALYZING_STALE_SECONDS) to rename the stale marker before the unlink,
so reprocess respawns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(steward): demote health pass to report-only; fix stale-marker reprocess deadlock
The deterministic steward health pass no longer auto-fires per-segment
reprocesses. `run_recipe_pass` becomes report-only (zero POSTs, zero
recipe.outcome rows; keeps its three empty-list keys for the heartbeat
pass event). Genuinely-pending segments are already re-run by the daily
sensing pre-phase, so firing remediated nothing the pipeline doesn't
handle — it only widened latency, at the cost of escalation/re-verify
bookkeeping that existed solely to track fired repairs.
Removed: the fire/detect/re-verify/escalation machinery and every
escalated_targets render branch; the unused for_range health_report
gather; the always-empty `## Trends (last 7d)` section (constant,
renderer, validator, bootstrap default, fixture); and the unmapped
reprocess_stale suggested-action (Python, talent prompt, JSON schema).
Fixed the now-sole manual reprocess path's deadlock: a stale
`.analyzing_<modality>` marker classified FAILED was never cleared, so
create_analyzing_marker hit FileExistsError and never respawned. The
FAILED branch now calls repair_modality_markers (reusing
ANALYZING_STALE_SECONDS) to rename the stale marker before the unlink,
so reprocess respawns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(import): one native-client staging contract (client_item_id, /meta, canonical source)
Evolve the import staging API in place so web, `sol import`, and native
clients stage through one strengthened contract.
- /save + /save-path now REQUIRE client_item_id (idempotency key). Staged
scan gives idempotent replay (same id + same content) vs 409
import_client_id_conflict (same id, different content). Replay of an
already-started item recommends do_not_start.
- Duplicates are terminal: content matching an imported manifest OR an
already-staged item returns status=duplicate / recommended_action=
do_not_start and is NOT staged again. /start refuses a saved terminal
duplicate regardless of force.
- /save returns a versioned summary: schema_version, status, replay, path,
timestamp, client_item_id, source (server-inferred canonical
audio|image|document|text), facet, setting, recommended_action, a metadata
bag (incl. flexible client bag), diagnostics (folds timestamp_detection_*),
and a duplicate object when applicable. /save-path shares the shape.
- /api/facet renamed to /api/meta (clean break, no alias).
- /start is saved-metadata-authority (ignores request source/facet/setting)
and routes via saved source_hint. recording/quick/apple never reach the
wire or the importer as source values.
Conservative PDF: canonical source reports "document" for PDFs as metadata,
but generic/quick PDF processing is unchanged (still the text path).
Intentional BREAKING changes to import.save, re-pinned via `make openapi`:
required client_item_id; top-level timestamp_detection_* moved under
diagnostics. Adds import.savePath + import.meta ops. Native-client (iOS)
owners must update.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route mutation lanes through entity resolution boundary
Move the audited journal-writing name-resolution lanes onto record_entity_resolution so tier 5-8 matches persist ambiguity before any domain write and then reuse each lane's existing unresolved representation.
This prevents low-confidence names from creating duplicates, attaching speaker labels or voiceprints, or firing irreversible identity merges. In particular merge.py now stages ambiguous source entities, speakers.merge_names refuses before merge_entity(commit=True), and discovery exits before entity creation, voiceprint saves, labels, resolved-cluster writes, and candidate retroactive confirmation.
The import bridge checks resolved choices before the identical-content shortcut, so a staged low-confidence entity can be resolved and then applied on re-ingest without leaving received, id_map, or staging state incoherent.
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor(import): drain REST-conventions buckets + own staging-dir move
Bring solstone/apps/import/routes.py to REST conventions and close one
C12 layer-hygiene residual:
- journal_source_manifest: abort(404) -> error_response(INVALID_REQUEST_VALUE,
status=404). Protocol clients (observe/export.py::_query_manifest,
transfer.py) read only status + .text on non-200, so unaffected.
- api_journal_source_list (GET /api/journal-sources/list): bare top-level
array -> respond_collection envelope {"items": [...], "total": N}. No
production consumer; only the test referenced it.
- import_start: route the timestamp-change staging-dir rename through a new
imports/** owner, move_import() in think/importers/utils.py (single atomic
rename, typed FileNotFoundError/FileExistsError mapped to the existing
IMPORT_NOT_FOUND/IMPORT_CONFLICT reasons). routes.py no longer writes
imports/** inline. Persisted shapes byte-identical; file_path metadata
update stays in the route via the existing owner writer.
- Remove the two solstone/apps/import/routes.py entries (abort, bare-array)
from check_api_conventions.py ALLOWLIST.
- Tests: raw-array -> envelope; manifest-404 mock mirrors production and
asserts the JSON envelope; new move_import unit tests + import_start
end-to-end route tests (success move, missing-source, target-exists).
CPO signals:
1. Stale spec number: parent spec said "import abort: 2"; live tree had 1
(the lone abort at routes.py:1012). Corrected.
2. observer bare-array x2 stays allowlisted: it is a peer-sync WIRE PROTOCOL
(consumed by observe/transfer.py::_query_remote_segments iterating the raw
top-level array). Retargets R3 -> R4, to be re-owned with protocol_version
negotiation. Not drained here.
3. `import list-staged` (CLI read for staged entity/facet/config items) has no
HTTP route -- genuine read-skew, candidate R4 fill-write, not built here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(indexer): retire the Python command-write path
Every write-bearing journal indexer invocation now runs solstone-core indexer; query, count, display, and interactive search stay Python.
Delete the core.indexer / core.indexer_on_decline selector, the 69 decline-fallback, and the Python CLI write blocks rather than deprecating them; the command no longer reads either key.
Remove reset_journal_index and its export.
Write-bearing commands on hosts with no compatible solstone-core wheel, including musl Linux and Intel macOS, exit 78 before native execution, Python writes, or query.
Make --rescan-file with --rescan/--rescan-full exit 64 before any side effect, replacing the old partial-side-effect ordering.
Rename native_seam.py to native.py.
Teach the indexer differential harness the native edge-extraction-skip stderr wording, mirroring the existing native markdown-sanitize rule, and point the fixture-corpus commands at the retired-selector-free journal indexer invocation.
refactor(indexer): retire the Python command-write path
Every write-bearing journal indexer invocation now runs solstone-core indexer; query, count, display, and interactive search stay Python.
Delete the core.indexer / core.indexer_on_decline selector, the 69 decline-fallback, and the Python CLI write blocks rather than deprecating them; the command no longer reads either key.
Remove reset_journal_index and its export.
Write-bearing commands on hosts with no compatible solstone-core wheel, including musl Linux and Intel macOS, exit 78 before native execution, Python writes, or query.
Make --rescan-file with --rescan/--rescan-full exit 64 before any side effect, replacing the old partial-side-effect ordering.
Rename native_seam.py to native.py.
Teach the indexer differential harness the native edge-extraction-skip stderr wording, mirroring the existing native markdown-sanitize rule, and point the fixture-corpus commands at the retired-selector-free journal indexer invocation.
refactor(indexer): retire the Python command-write path
Every write-bearing journal indexer invocation now runs solstone-core indexer; query, count, display, and interactive search stay Python.
Delete the core.indexer / core.indexer_on_decline selector, the 69 decline-fallback, and the Python CLI write blocks rather than deprecating them; the command no longer reads either key.
Remove reset_journal_index and its export.
Write-bearing commands on hosts with no compatible solstone-core wheel, including musl Linux and Intel macOS, exit 78 before native execution, Python writes, or query.
Make --rescan-file with --rescan/--rescan-full exit 64 before any side effect, replacing the old partial-side-effect ordering.
Rename native_seam.py to native.py.
Teach the indexer differential harness the native edge-extraction-skip stderr wording, mirroring the existing native markdown-sanitize rule, and point the fixture-corpus commands at the retired-selector-free journal indexer invocation.
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
fix(doctor): recognize the app-owned sol and journal launchers as healthy
install_guard._RE_MARKER accepts only numeric managed-wrapper versions 1-7, so the regular file the macOS app writes at ~/.local/bin/{sol,journal} fell through parse_wrapper to AliasState.FOREIGN. Every correct app install therefore got a permanent, false stale_alias_symlink warning telling the owner to run journal setup, which would replace a healthy launcher.
Add a narrow doctor-health recognizer, install_guard.is_app_owned_child_launcher, that requires exact equality against the rendered canonical app-owned-child body naming the literal dirname(sys.executable)/<binary> target, never resolved, plus that target being present and executable. Wire it into doctor.stale_alias_symlink_check only.
Mutation ownership is unchanged. check_alias, check_alias_detail, cmd_check, cmd_install, cmd_uninstall, provision_wrappers, and setup's wrapper/clean-uninstall steps still treat the file as FOREIGN; no AliasState member was added and import_install_guard's 2-tuple signature is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(providers): install CUDA llama-server from pinned tarballs
Replace the owner-side OCI image pull for the CUDA llama-server with per-platform sha256-pinned tarballs hosted on updates.solstone.app. The CUDA lane now follows the same download, sha256 verify, safe extract, required-files check, manifest write, and atomic publish machinery as the Vulkan lane.
Key CUDA installs by the tarball sha256, write a standard llama-server-cuda provider manifest, and remove legacy OCI digest-keyed trees only after a successful replacement publish. Fit reports now account for the pinned CUDA tarball size as a known artifact.
Tests cover pin coverage, pin-present resolver selection, no-pin Vulkan fallback, sha256 fail-closed, required-file failures, traversal rejection, manifest inventory, cohort convergence, and legacy-tree cleanup.
Co-Authored-By: OpenAI Codex <codex@openai.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor(cli): move eleven local-only commands from sol to journal surface
Flip engage, journal-stats, indexer, top, health, streams, segment, providers, reprocess, restart-convey, and observer from access to service in the COMMANDS registry, and trim ACCESS_HELP_GROUPS to the surviving sol peer set.
Re-home production subprocess callers from the sol binary to journal for indexer, providers, and journal-stats call sites, including runner docstrings.
Sweep human- and agent-facing sol <cmd> references to journal <cmd> across docs, talent prompts, the health skill, Makefile, README, INSTALL, AGENTS.md, the stats dashboard, and .codex exec-policy rules, including a journal prefix allow rule.
Leave sol call health, sol call settings providers, and access stayers intact.
Broaden the migration test to scan .js and .rules, retarget positive expectations to stayers, and add a registry-derived guard that prevents production service commands from dispatching via sol.
Add one Unreleased changelog bullet with no version bump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(think): make daily-unit failure caps terminal for day completion
A daily unit at its deterministic-failure cap was permanently skipped for dispatch, but the day-completion predicate still demanded every applicable unit be complete. That meant health/daily.updated was never written, the catchup scheduler re-queued the day forever, and journal doctor reported it stuck indefinitely.
Install the invariant that a unit's failure cap is terminal: the same predicate that stops dispatch marks the unit terminal-degraded for day completion. Day completion means all applicable units terminal, not all units succeeded. Degradation is terminal but visible.
Recalibrate deterministic caps so schema_invalid moves to cap 3 while every other deterministic reason stays at 2. schema_invalid measured 24.3% per-call failure on the affected talent (87 complete / 28 schema_invalid since the local cutover), with same-day fail-then-pass observed on 20260723 for entity_observer:vconic, failed 00:24 and completed 00:36. The other reasons are unmeasured and deliberately kept tight.
Replace DETERMINISTIC_FAILURE_THRESHOLD outright with the cogitate_policy.py cap SOT, DETERMINISTIC_FAILURE_CAPS plus failure_capped. Extract thinking.evaluate_daily_completion as the pure predicate and thinking.finalize_day_completion as the marker-write, payload, and log seam from the inline main() block. Surface capped units on the daily_complete payload, the BacklogDay complete-day path, and the journal doctor caught-up check.
retry_on_deterministic_failure deliberately affects dispatch only; the completion predicate treats capped units as terminal regardless of the flag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(sol): remove Python sol skills implementation
Relocate _discover_project_sources and _ROUTER_SKILL_NAMES into solstone/think/doctor.py rather than sharing them, because doctor is still Python and must not shell out to the native binary. They are knowingly duplicated with core/crates/solstone-core-sol/src/skills.rs, and that duplication ends when doctor itself is ported.
Exactly one assertion is knowingly dropped for a reason other than the build verb removal: the duplicate-router-name ValueError branch from the original skills_cli.discover_project_sources. It is unreachable now that the required payload names are a declared constant of two distinct literals.
The build verb is removed from the CLI surface as a deliberate, operator-approved change. Its sys.path.insert plus from scripts import build_skill_references scaffolding is deleted outright, not ported. make skills invokes scripts/build_skill_references.py directly as of the preceding lode.
Delete scripts/capture_skills_parity_oracle.py in this same commit. The harness drives solstone.think.skills_cli directly: it subprocesses python -m solstone.think.skills_cli and monkeypatches skills_cli.get_project_root and skills_cli.resources.files. Once that module is gone, the script cannot run at all; it would be dead code, not merely stale. Its provenance role is fully preserved: the harness and the 30 vectors it produced are committed together at 5240cfaaf, strictly before this deletion, so git log over core/fixtures/native-sol/skills-parity-v1/ still proves the vectors were captured from the live Python oracle. The frozen vectors themselves remain in the tree and continue to gate the native implementation.
Leave solstone/think/setup.py deliberately untouched. Its skills_user_command() and skills_journal_command() build python -m solstone.think.sol_cli skills install ..., but skills is in JOURNAL_ACCESS_ONLY_COMMANDS, so sol_cli rejects it with exit 2 and journal setup reports both skills_user and skills_journal as failures. That defect predates this lode and is left for a follow-up; tests/test_setup.py and tests/test_setup_jsonl.py pin the current argv shape and pass unmodified.
Co-Authored-By: OpenAI Codex <codex@openai.com>
refactor(indexer): retire the Python command-write path
Every write-bearing journal indexer invocation now runs solstone-core indexer; query, count, display, and interactive search stay Python.
Delete the core.indexer / core.indexer_on_decline selector, the 69 decline-fallback, and the Python CLI write blocks rather than deprecating them; the command no longer reads either key.
Remove reset_journal_index and its export.
Write-bearing commands on hosts with no compatible solstone-core wheel, including musl Linux and Intel macOS, exit 78 before native execution, Python writes, or query.
Make --rescan-file with --rescan/--rescan-full exit 64 before any side effect, replacing the old partial-side-effect ordering.
Rename native_seam.py to native.py.
Teach the indexer differential harness the native edge-extraction-skip stderr wording, mirroring the existing native markdown-sanitize rule, and point the fixture-corpus commands at the retired-selector-free journal indexer invocation.
feat(speakers): candidate-backed owner detection
Replace the full-journal HDBSCAN owner-detection scan with deterministic selection plus capped, stream-interleaved expansion of one candidate from the recurring speaker candidate pool. Detection cost is now O(cap), independent of journal size, so it no longer blows the 20s HTTP read timeout.
Move owner_detection_ready() from solstone/think/awareness.py into solstone/apps/speakers/owner.py. It no longer triggers clustering, and the think-to-app layer inversion is gone.
Make load_owner_embedding_inventory() count NPZ rows from the NPY header instead of deflating every embedding matrix, measured 88.5s -> 0.93s on a synthetic 3000-segment x 2-source journal. sol call speakers status owner and GET /api/owner/status no longer pay a full materializing scan. No cache file, no new journal artifact.
Add load_npz_row_count() in solstone/think/journal_io/npz.py and CandidateTracker.load_all_candidates() as a read accessor so owner.py reads the pool through its write-owner (L2).
Short-circuit detect on a confirmed centroid and on an existing candidate, so reruns do not recompute or rewrite owner_candidate.npz.
Set low-quality source to candidate_pool and delete the hdbscan owner path, MAX_EMBEDDINGS, _subsample_embeddings, count_segments_with_embeddings, and the HDBSCAN test scaffolding. discovery.py keeps its own HDBSCAN pass.
Keep owner_candidate.npz keys, the awareness voiceprint payload, and workspace.html unchanged.
Route mutation lanes through entity resolution boundary
Move the audited journal-writing name-resolution lanes onto record_entity_resolution so tier 5-8 matches persist ambiguity before any domain write and then reuse each lane's existing unresolved representation.
This prevents low-confidence names from creating duplicates, attaching speaker labels or voiceprints, or firing irreversible identity merges. In particular merge.py now stages ambiguous source entities, speakers.merge_names refuses before merge_entity(commit=True), and discovery exits before entity creation, voiceprint saves, labels, resolved-cluster writes, and candidate retroactive confirmation.
The import bridge checks resolved choices before the identical-content shortcut, so a staged low-confidence entity can be resolved and then applied on re-ingest without leaving received, id_map, or staging state incoherent.
fix(build): rebuild the native binary when Rust sources change
`solstone-core` is a maturin package whose Rust sources live outside its
package directory — maturin is pointed at ../../core. Nothing that watches
the build watched there, so two gates were both open:
- uv's default cache key for a path dependency is that package's own
pyproject.toml, so `uv sync` reported "no changes" after a Rust-only
edit and left the previously built binary installed;
- `.installed`'s prerequisites did not move either, so make short-circuited
and never called uv at all. PYTEST is $(VENV_BIN)/pytest, not
`uv run pytest`, so nothing else triggered a sync.
Any environment whose .venv predated a native change kept executing the
older binary while `make ci` reported green. test_make_skills_idempotent is
the only test that copies and runs .venv/bin/solstone-core, so it was the
only one to notice — a pre-port binary has no native `skills` command,
routes to the removed Python delegation, and exits 78.
Declare the real build inputs as uv cache-keys so a sync rebuilds, and add
a content-hashed .rust-core-hash stamp so `make install` runs a sync at all
after a Rust-only change. Both halves are required; cache-keys alone never
fires because make short-circuits first.
Also stop swallowing the binary's own explanation in the idempotency test.
check=True raises CalledProcessError, whose pytest summary carries the exit
status and nothing else, discarding the one sentence that named the cause.
refactor(import): drain REST-conventions buckets + own staging-dir move
Bring solstone/apps/import/routes.py to REST conventions and close one
C12 layer-hygiene residual:
- journal_source_manifest: abort(404) -> error_response(INVALID_REQUEST_VALUE,
status=404). Protocol clients (observe/export.py::_query_manifest,
transfer.py) read only status + .text on non-200, so unaffected.
- api_journal_source_list (GET /api/journal-sources/list): bare top-level
array -> respond_collection envelope {"items": [...], "total": N}. No
production consumer; only the test referenced it.
- import_start: route the timestamp-change staging-dir rename through a new
imports/** owner, move_import() in think/importers/utils.py (single atomic
rename, typed FileNotFoundError/FileExistsError mapped to the existing
IMPORT_NOT_FOUND/IMPORT_CONFLICT reasons). routes.py no longer writes
imports/** inline. Persisted shapes byte-identical; file_path metadata
update stays in the route via the existing owner writer.
- Remove the two solstone/apps/import/routes.py entries (abort, bare-array)
from check_api_conventions.py ALLOWLIST.
- Tests: raw-array -> envelope; manifest-404 mock mirrors production and
asserts the JSON envelope; new move_import unit tests + import_start
end-to-end route tests (success move, missing-source, target-exists).
CPO signals:
1. Stale spec number: parent spec said "import abort: 2"; live tree had 1
(the lone abort at routes.py:1012). Corrected.
2. observer bare-array x2 stays allowlisted: it is a peer-sync WIRE PROTOCOL
(consumed by observe/transfer.py::_query_remote_segments iterating the raw
top-level array). Retargets R3 -> R4, to be re-owned with protocol_version
negotiation. Not drained here.
3. `import list-staged` (CLI read for staged entity/facet/config items) has no
HTTP route -- genuine read-skew, candidate R4 fill-write, not built here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(think): make daily-unit failure caps terminal for day completion
A daily unit at its deterministic-failure cap was permanently skipped for dispatch, but the day-completion predicate still demanded every applicable unit be complete. That meant health/daily.updated was never written, the catchup scheduler re-queued the day forever, and journal doctor reported it stuck indefinitely.
Install the invariant that a unit's failure cap is terminal: the same predicate that stops dispatch marks the unit terminal-degraded for day completion. Day completion means all applicable units terminal, not all units succeeded. Degradation is terminal but visible.
Recalibrate deterministic caps so schema_invalid moves to cap 3 while every other deterministic reason stays at 2. schema_invalid measured 24.3% per-call failure on the affected talent (87 complete / 28 schema_invalid since the local cutover), with same-day fail-then-pass observed on 20260723 for entity_observer:vconic, failed 00:24 and completed 00:36. The other reasons are unmeasured and deliberately kept tight.
Replace DETERMINISTIC_FAILURE_THRESHOLD outright with the cogitate_policy.py cap SOT, DETERMINISTIC_FAILURE_CAPS plus failure_capped. Extract thinking.evaluate_daily_completion as the pure predicate and thinking.finalize_day_completion as the marker-write, payload, and log seam from the inline main() block. Surface capped units on the daily_complete payload, the BacklogDay complete-day path, and the journal doctor caught-up check.
retry_on_deterministic_failure deliberately affects dispatch only; the completion predicate treats capped units as terminal regardless of the flag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(convey): remove web-UI password, login, and session auth
port 5015 is loopback-only, and mtls on 7657 is the sole network transport as of 0.6.0, so the web password gated nothing. remove the login form, flask session-cookie login, http basic auth, the trust_localhost localhost bypass, the journal password cli, and the convey.secret/app.secret_key flask-session machinery.
refactor the require_login gate to require_access while keeping private link revocation auth and the first-run /init onboarding redirect. there is no migration: orphan convey.password_hash, convey.secret, and trust_localhost keys in existing journal.json files are ignored, and _NEVER_TRANSFER_PATHS still scrubs password_hash and secret on export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
fix(observe,providers): stop reporting expected negatives as failures
A handler child reaped with a signal exit during our own signalled shutdown was classified as a handler failure: an ERROR line, an owner-facing callosum notification, and a diagnostics-beacon increment. It is now classified as cancelled. The segment's residual error accounting is unchanged, because the segment genuinely was not fully observed; what was untrue was calling our own shutdown a handler defect.
The guard keys on _stopping, never on the sign of the exit code, so a kernel-killed handler outside a shutdown still reports normally. _on_handler_timeout is deliberately untouched: it fires from the watchdog runtime cap, and a handler that blew its cap is a genuine failure.
probe_nvidia_gpu() on a host with no nvidia-smi binary emitted a WARNING every call, hundreds of lines across separate talent subprocesses, for a host that simply has no NVIDIA GPU. It now returns the undetected result without spawning or logging anything at any level, mirroring detect_nvidia_unified_memory(). The OSError handler and its WARNING stay as the non-ENOENT fallback.
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
fix(think): make schema-invalid generate output a terminal error
_execute_generate previously wrote the model output to output_path whenever it was truthy and then emitted finish. get_use_end_state() classifies finish as a clean talent.complete, so a local model that emitted schema-violating JSON could both corrupt the output artifact and count as a successful daily completion. Cloud providers enforce schemas server-side, so this primarily affects the local provider path, which is the default engine direction.
The new pre-write gate validates the candidate that would actually be written, using the load-bearing predicate output_path and result and not _output_valid_for_schema(result, ...). Using _schema_validation_clean here would have short-circuited on the raw-text advisory and fired for pulse and steward, whose post-hooks repair unparseable raw output into a valid default; that would turn graceful degradation into a terminal error. _schema_validation_clean is unchanged and keeps its one caller in the clean-provenance predicate, where the conservative raw short-circuit is correct.
The output_path and result prefix is intentional. It excludes the story post-hook talents conversation, work, and event, which return an empty string on every path because their real output is a merge into the activity record.
The error event includes schema_validation verbatim when present, but that field describes the raw provider text rather than the rejected post-hook candidate. It can therefore read valid: true when a hook produced invalid output. The human error message derives from errors[0] only when the raw text itself was invalid; otherwise it uses a generic fallback.
schema_invalid now joins DETERMINISTIC_FAILURE_REASON_CODES so the existing threshold-2 backoff applies. The first schema-invalid run still gets a normal re-dispatch, so a stochastic local model that recovers next cadence is not penalized; only a talent that fails twice consecutively backs off. The set comment now reflects that these include high-recurrence stochastic failures, since "will crash identically" was already false for no_output.
Fenced JSON is now an honest failure. tests/test_markdown_fence_strip.py::test_generate_json_output_not_stripped used to assert silent success for a fenced JSON payload; it now asserts terminal schema_invalid while preserving the original invariant that output: json is never fence-stripped. JSON fence-stripping remains a deliberate follow-up, not part of this change.
The _output_valid_for_schema warning no longer says cached, since that was already wrong at two of its three call sites. _execute_with_tools also documents that no cogitate talent declares output: json or a schema today, and any future gate there should mirror the generate path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(import): one native-client staging contract (client_item_id, /meta, canonical source)
Evolve the import staging API in place so web, `sol import`, and native
clients stage through one strengthened contract.
- /save + /save-path now REQUIRE client_item_id (idempotency key). Staged
scan gives idempotent replay (same id + same content) vs 409
import_client_id_conflict (same id, different content). Replay of an
already-started item recommends do_not_start.
- Duplicates are terminal: content matching an imported manifest OR an
already-staged item returns status=duplicate / recommended_action=
do_not_start and is NOT staged again. /start refuses a saved terminal
duplicate regardless of force.
- /save returns a versioned summary: schema_version, status, replay, path,
timestamp, client_item_id, source (server-inferred canonical
audio|image|document|text), facet, setting, recommended_action, a metadata
bag (incl. flexible client bag), diagnostics (folds timestamp_detection_*),
and a duplicate object when applicable. /save-path shares the shape.
- /api/facet renamed to /api/meta (clean break, no alias).
- /start is saved-metadata-authority (ignores request source/facet/setting)
and routes via saved source_hint. recording/quick/apple never reach the
wire or the importer as source values.
Conservative PDF: canonical source reports "document" for PDFs as metadata,
but generic/quick PDF processing is unchanged (still the text path).
Intentional BREAKING changes to import.save, re-pinned via `make openapi`:
required client_item_id; top-level timestamp_detection_* moved under
diagnostics. Adds import.savePath + import.meta ops. Native-client (iOS)
owners must update.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(talent): schema-bounded JSON for morning briefing
morning_briefing now emits schema-bounded JSON via the new morning_briefing.schema.json file. The schema is fully bounded and does not require any check_schema_bounds.py ALLOWLIST entry. A day-level formatter registry entry renders the JSON output back to markdown for the search index and the identity CLI, preserving the five ## headings and the morning_briefing indexer agent string that entities/context.py's NOISE_AGENTS depends on.
morning_briefing_path() previously hard-coded output_format="md", the single hard-coded lie in the path authority for this talent. It now derives the extension from the talent's declared output format through a cached _briefing_output_format() helper, so briefing_cmd's day_dirs() loop does not re-read the talent config once per day.
The consumer migration is a clean break, not a compatibility shim. _load_briefing_md and _BRIEFING_SECTIONS are deleted, and Home, voice, and the identity CLI all read through the new shared solstone/think/briefing.py loader and renderers. This incidentally removes the solstone.think.voice.tools -> solstone.apps.home.routes import, a think-to-apps dependency inversion logged as a follow-up during recon.
A post_process hook was considered and declined. The empirical finding was that _run_post_hooks runs before _write_output on both talents.py:1375 and talents.py:1673, while _schema_validation_clean at talents.py:1087 rejects on the provider's gen_result["schema_validation"] before re-validating the post-hooked string, so a post-hook cannot repair a required key the model omitted. Because every schema root is additionalProperties: false, a post-hook cannot inject an undeclared key either. OpenAI strict mode forbids optional properties, so "declared but not required" is unavailable; the model must emit the key regardless, leaving the hook's only residual value as overwriting values the model was already forced to produce. talents.py:1822 also pops template_vars off modifications and never stores it on config, so a post-hook cannot even see the pre-hook packet without inventing a new pre-hook return-key convention that exists nowhere else in the repo. Instead, the pre-hook collapses generated, model, source_counts, source_gaps, and coverage_preamble into one $briefing_metadata template var built with json.dumps(..., indent=2), retiring the two-space YAML fragment _render_source_counts used to emit and removing the JSON-escaping hazard for the preamble prose. date is dropped entirely because the path encodes the day.
Only lowercase $briefing_metadata is safe in the prompt. _apply_template_vars at talents.py:852-861 registers a .capitalize() alias that would lowercase the rest of the JSON string, so the prompt uses the lowercase token only and a test pins that constraint.
tests/eval_schemas.py now runs schema_path cases through hydrate_runtime_enums so the eval validates the runtime schema the provider actually receives. Without this, the reading[].facet __RUNTIME_FACETS__ sentinel would force the eval model to emit that literal string and the new golden case could never pass. reading[].facet carries both the sentinel enum and a maxLength because hydrate_runtime_enums drops the enum key on a zero-facet journal and the string would otherwise ship to the provider unbounded.
_strip_outer_markdown_fence runs only for output_format == "md" at talents.py:1658-1668, so JSON output is not fence-stripped. The prompt's explicit no-fence instruction is the only mitigation, matching every other output: json talent. This is not a new condition, but it is recorded here so the next reader does not assume the runner strips JSON fences.
max_output_tokens: 8192 now matches documents; the talent previously inherited the 8192 * 6 default silently. degradation_check: true is retained for the same reason 9654c0d0 retained it on documents: four other json-output talents set it, so briefing is not special. A near-empty briefing sits under the MIN_OUTPUT_TOKENS = 300 floor and could be falsely flagged degraded on a sparse day; this remains a follow-up risk.
The two API baselines flip legitimately. tests/baselines/api/sol/talents-day.json moves output_format from md to json. tests/baselines/api/stats/stats.json moves output from md to json and additionally gains schema and max_output_tokens because that API exposes raw prompt frontmatter and every other talent with those fields already shows them there. Nothing else in either file moved.
Behavior is otherwise held constant across the web contract. briefing_sections stays a dict of markdown strings and briefing_needs_deduped stays an array of strings, so home.js and workspace.html are untouched. Needs items are now objects carrying text plus a sol:// source_id, which needs_dedup_key resolves by identity; inline [label](sol://...) links stay inside text so the existing parse_sol_sources fallback keeps working. _briefing_summary now counts meetings from structured your_day items with a non-empty time rather than regexing - **HH:MM**, and produces a byte-identical string for equivalent content.
One deliberate behavior change remains: reading links now resolve to facet=<slug>, for example facet=work, where the model previously wrote **Work** and produced facet=Work. No test pinned the old casing, and the slug is what /app/search expects.
The local Qwen provider is not installed on this machine, so the eval was not executed and no pass is claimed. The command below was run with this output.
```
$ make eval-schemas; echo "exit=$?"
.venv/bin/python tests/eval_schemas.py
Local schema eval requires the bundled local provider. Run `journal install-provider local`, then start it with `journal start` (or `journal service start` for an installed service).
make: *** [Makefile:234: eval-schemas] Error 2
exit=2
```
Follow-ups deliberately not done: pulse.schema.json still has 5 unbounded nodes and steward.schema.json still has 2, both allowlisted under their "morning_briefing follow-on lode" reason, following the convention that the string names the lode a following lode will bound. solstone/apps/speakers/status.py's meetings_files counter still has the same wrong-path bug 9654c0d0 fixed for screen_files. docs/design/yesterdays-processing-card.md and docs/design/voice-server.md still describe frontmatter-based briefing reads in narrative prose beyond the one-line path references corrected here. No confidence field was added to briefing items; evidence strength stays prose hedging because a field would change rendering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(talent): schema-bounded JSON for morning briefing
morning_briefing now emits schema-bounded JSON via the new morning_briefing.schema.json file. The schema is fully bounded and does not require any check_schema_bounds.py ALLOWLIST entry. A day-level formatter registry entry renders the JSON output back to markdown for the search index and the identity CLI, preserving the five ## headings and the morning_briefing indexer agent string that entities/context.py's NOISE_AGENTS depends on.
morning_briefing_path() previously hard-coded output_format="md", the single hard-coded lie in the path authority for this talent. It now derives the extension from the talent's declared output format through a cached _briefing_output_format() helper, so briefing_cmd's day_dirs() loop does not re-read the talent config once per day.
The consumer migration is a clean break, not a compatibility shim. _load_briefing_md and _BRIEFING_SECTIONS are deleted, and Home, voice, and the identity CLI all read through the new shared solstone/think/briefing.py loader and renderers. This incidentally removes the solstone.think.voice.tools -> solstone.apps.home.routes import, a think-to-apps dependency inversion logged as a follow-up during recon.
A post_process hook was considered and declined. The empirical finding was that _run_post_hooks runs before _write_output on both talents.py:1375 and talents.py:1673, while _schema_validation_clean at talents.py:1087 rejects on the provider's gen_result["schema_validation"] before re-validating the post-hooked string, so a post-hook cannot repair a required key the model omitted. Because every schema root is additionalProperties: false, a post-hook cannot inject an undeclared key either. OpenAI strict mode forbids optional properties, so "declared but not required" is unavailable; the model must emit the key regardless, leaving the hook's only residual value as overwriting values the model was already forced to produce. talents.py:1822 also pops template_vars off modifications and never stores it on config, so a post-hook cannot even see the pre-hook packet without inventing a new pre-hook return-key convention that exists nowhere else in the repo. Instead, the pre-hook collapses generated, model, source_counts, source_gaps, and coverage_preamble into one $briefing_metadata template var built with json.dumps(..., indent=2), retiring the two-space YAML fragment _render_source_counts used to emit and removing the JSON-escaping hazard for the preamble prose. date is dropped entirely because the path encodes the day.
Only lowercase $briefing_metadata is safe in the prompt. _apply_template_vars at talents.py:852-861 registers a .capitalize() alias that would lowercase the rest of the JSON string, so the prompt uses the lowercase token only and a test pins that constraint.
tests/eval_schemas.py now runs schema_path cases through hydrate_runtime_enums so the eval validates the runtime schema the provider actually receives. Without this, the reading[].facet __RUNTIME_FACETS__ sentinel would force the eval model to emit that literal string and the new golden case could never pass. reading[].facet carries both the sentinel enum and a maxLength because hydrate_runtime_enums drops the enum key on a zero-facet journal and the string would otherwise ship to the provider unbounded.
_strip_outer_markdown_fence runs only for output_format == "md" at talents.py:1658-1668, so JSON output is not fence-stripped. The prompt's explicit no-fence instruction is the only mitigation, matching every other output: json talent. This is not a new condition, but it is recorded here so the next reader does not assume the runner strips JSON fences.
max_output_tokens: 8192 now matches documents; the talent previously inherited the 8192 * 6 default silently. degradation_check: true is retained for the same reason 9654c0d0 retained it on documents: four other json-output talents set it, so briefing is not special. A near-empty briefing sits under the MIN_OUTPUT_TOKENS = 300 floor and could be falsely flagged degraded on a sparse day; this remains a follow-up risk.
The two API baselines flip legitimately. tests/baselines/api/sol/talents-day.json moves output_format from md to json. tests/baselines/api/stats/stats.json moves output from md to json and additionally gains schema and max_output_tokens because that API exposes raw prompt frontmatter and every other talent with those fields already shows them there. Nothing else in either file moved.
Behavior is otherwise held constant across the web contract. briefing_sections stays a dict of markdown strings and briefing_needs_deduped stays an array of strings, so home.js and workspace.html are untouched. Needs items are now objects carrying text plus a sol:// source_id, which needs_dedup_key resolves by identity; inline [label](sol://...) links stay inside text so the existing parse_sol_sources fallback keeps working. _briefing_summary now counts meetings from structured your_day items with a non-empty time rather than regexing - **HH:MM**, and produces a byte-identical string for equivalent content.
One deliberate behavior change remains: reading links now resolve to facet=<slug>, for example facet=work, where the model previously wrote **Work** and produced facet=Work. No test pinned the old casing, and the slug is what /app/search expects.
The local Qwen provider is not installed on this machine, so the eval was not executed and no pass is claimed. The command below was run with this output.
```
$ make eval-schemas; echo "exit=$?"
.venv/bin/python tests/eval_schemas.py
Local schema eval requires the bundled local provider. Run `journal install-provider local`, then start it with `journal start` (or `journal service start` for an installed service).
make: *** [Makefile:234: eval-schemas] Error 2
exit=2
```
Follow-ups deliberately not done: pulse.schema.json still has 5 unbounded nodes and steward.schema.json still has 2, both allowlisted under their "morning_briefing follow-on lode" reason, following the convention that the string names the lode a following lode will bound. solstone/apps/speakers/status.py's meetings_files counter still has the same wrong-path bug 9654c0d0 fixed for screen_files. docs/design/yesterdays-processing-card.md and docs/design/voice-server.md still describe frontmatter-based briefing reads in narrative prose beyond the one-line path references corrected here. No confidence field was added to briefing items; evidence strength stays prose hedging because a field would change rendering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(sandbox): machine-facing journal sandbox-profile lifecycle
Add the journal-host command journal sandbox-profile with describe, prepare, apply, status, and disable subcommands, registered with surface="service".\n\nKeep the lifecycle journal-local only: it composes existing services/ and backup/ owner functions without calling portal, broker, relay-retirement, or production-storage paths. Validate the read-only .solstone-sandbox.json marker before any journal side effect, and persist run-owned non-secret intent at health/sandbox-profile/intent.json with an L2 write-owner row.\n\nDefine the contract version 1 envelope with closed vocabularies and ok=0 / degraded=1 / error=2 / cleanup_failed=3 exit mapping. Read handoffs from stdin only, cap them at 64 KiB, validate them strictly, and keep secrets out of argv, stdout, stderr, logs, and exception text.\n\nFollow through on registry generation by bumping EXPECTED_SERVICE_COMMANDS_COUNT from 42 to 43 and regenerating the native journal-host command inventory.\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(convey): escape the notification badge
_createCard interpolated n.badge raw into innerHTML while every neighbouring field, including n.app, n.title, and n.message, was escaped, so a caller could inject markup through the badge. The value is caller-controlled end to end: sol notify --badge is free-form and websocket.js forwards raw callosum payloads into show().
_updateCard already assigned the badge with textContent, so initial render and update render disagreed. This aligns them on the safe behaviour.
Found while auditing the icon-contract change. It is the same class of defect that change closes for icon, in the same renderer, but a different field, hence a separate commit.
The test helper assertNoPayload was tightened to match event-handler markup only inside a real tag, because correctly escaped output legitimately contains the literal text onerror.
test(observer): harden the segment-freshness coupling and divergence guards
The AC2 coupling guard was weaker than it looked. It only detected observer["last_segment"] = ... subscript writes and confirmed coupling via an 8-line raw text window, so observer.update({"last_segment": ...}) could slip through and a nearby comment could satisfy the check. The guard is now AST-based, detects both mutation forms, and scopes the coupled-write search to the enclosing function body. Exact-key-literal matching is retained so last_segment_received_at and last_segment_day, which both contain the substring last_segment, cannot self-satisfy it.
The guard is now itself tested. Synthetic-source cases assert it raises on an uncoupled subscript write and on an uncoupled .update() write, so the invariant cannot silently pass on everything.
AC5 divergence is now asserted at the rendered level. The record-level event-ingest test proved divergence on the ingest side, but no test asserted the display state AC5 names: a single row with a fresh Last Seen and an unknown Last Segment. The new fleet-view test asserts Status: connected, a populated Last Seen, and Last Segment — on the same row, so a formatter that swapped the two fields fails.
_last_segment_cell now derives its slice from the header instead of hardcoded offsets, so an unrelated column-width change fails at the real cause.
Gates run:
- make format — exit 0
- make test-only TEST=tests/test_observer_cli.py — exit 0, 41 passed
- make test-app APP=observer — exit 0, 268 passed
Full make ci was not rerun: this commit is test-only and the settled tree at 68eff2c10 already passed it, exit 0 with 14661 passed and 16 skipped.
test(backup): cover media offload restore
Cover restore engine behavior for daily-key restic calls, _default stream layout, operated append-only sessions, BYO hosted seams, free-space refusal, no-op folding, partial restore sweeps, rollback, and callosum staying untouched.
Pin the real restic contract that drove the design: restic restore <snapshot_id>:<segment_dir> --target <segment_dir> --include /<name> may exit 0 while restoring zero files for a missing include, so tests assert missing files are caught by existence, size, and sha256 verification.
Also cover the exact five offload routes, the three journal backup offload CLI verbs, last_restore state, device_total_bytes(), and resolve_segment_dir() default-stream behavior.
Guard Oura egress and narrow future scopes
The Oura egress guard now covers every auto-discovered oura*.py instead of only oura.py, including oura_auth.py where OAuth and loopback callback networking live. The allowlist is keyed by module, owner, and capability rather than line numbers, permits module-level network imports for oura_auth only, and preserves oura.py's lazy-import discipline.
The guard fails on later-added socket, TLS, HTTP, mail, subprocess, dynamic-import, __import__, os.system, os.popen, and unallowlisted browser escape hatches. A synthetic self-test feeds violations to prove the guard can actually fail.
Future Oura authorization requests drop email and personal while deliberately keeping metabolic. metabolic authorizes only blood_glucose, which stays partner-gated and unpolled.
Docs now spell out exact retention semantics, the reserved writer-less Oura stream identity, the scope table, lock and consent-expiry behavior, and owner remediation. They also state that dropping email/personal affects future authorization requests only and does not revoke scopes already granted on existing tokens; narrowing an existing token requires owner-present re-consent or revocation as an operator action.
(cherry picked from commit 26ad7301707c3c4c9359d8930483cf357942f3b7)
fix(tests): close the Oura egress guard's module-scope escape hatch
The guard allowlisted (oura_auth, <module>, module_network_import) and then
treated that single entry as permission for any module-scope capability except
dynamic_import. Because the check fell back to that broad rule, a future
module-level `import subprocess`, `import socket`, `import ssl`, `import
smtplib`, or an `os.system(...)` call in oura_auth.py would have been reported
as allowed — defeating the guard's stated contract of allowlisting by module,
function, and capability, and defeating its requirement to fail on sockets, TLS
primitives, mail clients, and subprocess escape hatches.
_allowed is now an exact (module, owner, capability) membership test with no
fallback, and the broad entry is replaced by exactly the three capabilities
oura_auth.py genuinely needs at module scope, matching its imports:
urllib.request (http_client), webbrowser (browser_open), and http.server
(loopback_http_server). urllib.parse remains covered by SAFE_IMPORTS. oura.py's
lazy-import discipline is unchanged.
Detection also missed several transports the boundary is supposed to cover, so
urllib3 and pycurl are now recognized as HTTP clients and imaplib and poplib as
mail clients.
The synthetic self-test — which exists to prove the guard can actually fail —
now feeds module-scope subprocess imports and calls, os.system, socket, smtplib,
and urllib3 through the harness and asserts each is reported as a violation.
Also corrects two design-doc statements that no longer matched the code: the
Oura sync backend is registered and implements save-mode sync rather than
raising as an unregistered skeleton, and oura.py keeps a lazy-import discipline
with egress confined to the allowlisted transport rather than having no network
imports at all.
test(oura): move real-process lock probes to the integration tier
Commit 91f8fd9f3 on origin/main established that tests using real external
processes belong in operator validation, not unit CI, adding the `integration`
marker and `PYTEST_UNIT_ARGS := -m "not integration and not performance"` to
`make test` / `test-cov` / `coverage`; 2cf9e9585 then pointed `make ci` at
`make test`. The health-lane branch predates that policy, so its two
cross-process Oura lock proofs arrived unmarked and would have run inside the
unit gate — precisely the class of probe the policy exiles. 704d53bb9 shows the
repo actively rewriting tests away from real-process mechanics, so leaving them
unmarked would regress a live policy.
Marking them `integration` alone, however, would leave `make ci` with no
exclusion proof at all, while the scope requires exclusion be demonstrated with
distinct processes *or file descriptors* — not only threads.
So both subprocess probes are marked `integration` (they still run under
`make test-integration`), and the exclusion proof is retained inside the unit
gate by two in-process tests that use distinct file descriptors. This is sound
because hold_lock uses fcntl.flock(LOCK_EX | LOCK_NB), which is per open-file
description: two separate open() calls genuinely exclude one another within a
single process, so this is a real exclusion proof and not a thread simulation.
test_private_import_lock_excludes_distinct_fds_in_one_process pins that
per-open-file-description semantics directly, and fails loudly if the primitive
is ever swapped for POSIX per-process record locking, under which a second
descriptor would silently succeed and invalidate the proof.
test_oura_sync_lock_timeout_with_same_process_distinct_fd drives save-mode sync
against a held lock and asserts the structured OuraSyncLockError names the
journal, lock path, and timeout, leaks no token, request URL, or health payload,
performs no fetch, and writes no bundle, raw, normalized, dedupe, manifest, or
cursor state.
Rebase floor-tier Qwen residency
Two consecutive release-gate runs of the installed b10068 artifact reached a 4,140 MiB peak. The first run had 95 samples, including three consecutive 4,140 MiB samples post-multimodal; schema validation was 10/10 at 0.922 expected-content preservation.
Rebase arithmetic: 4,140 + 7 MiB cushion = 4,147.
Derived requirement: 4,147 + 2,947 + 1,024 = 8,118.
8 GiB headroom: 8,192 - 8,118 = 74 MiB.
Placement arithmetic and the b10068 pin are unchanged.
feat: place Parakeet STT on CPU when GPU cannot co-locate
On Linux hosts where the bundled brain is resident on a single discrete GPU that cannot also hold a worst-case ordinary-segment Parakeet run, transcribe.parakeet-cpp.device: auto now resolves to CPU at parakeet-server launch.
That decision uses measured floor-tier brain residency 4541 MiB + Parakeet worst case 5022 MiB + 1024 MiB margin = 10587 MiB. As a result, 6/8/10 GiB cards place STT on CPU, while 11/12 GiB and up keep it on the GPU.
Every other state fails toward today's behavior: unknown VRAM, probe failure, unmeasured capable-tier residency, 2+ discrete GPUs, integrated or unified memory, or an inactive bundled-brain lane keep auto resolving exactly as before.
The placement is announced through the supervisor reason line, GPU-check detail in sol check and the local provider fit report, and journal/health/parakeet-cpp.placement so transcribed events and transcript headers report the real device placement, cpu/gpu, rather than literal auto. The checks remain ok because this is a hardware-fit fact, not a failure.
No config keys were added; transcribe.parakeet-cpp.device still validates as auto | cpu only.
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
Route mutation lanes through entity resolution boundary
Move the audited journal-writing name-resolution lanes onto record_entity_resolution so tier 5-8 matches persist ambiguity before any domain write and then reuse each lane's existing unresolved representation.
This prevents low-confidence names from creating duplicates, attaching speaker labels or voiceprints, or firing irreversible identity merges. In particular merge.py now stages ambiguous source entities, speakers.merge_names refuses before merge_entity(commit=True), and discovery exits before entity creation, voiceprint saves, labels, resolved-cluster writes, and candidate retroactive confirmation.
The import bridge checks resolved choices before the identical-content shortcut, so a staged low-confidence entity can be resolved and then applied on re-ingest without leaving received, id_map, or staging state incoherent.
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(schemas): strict-portable the 13 Class-A schemas + provider-parity guard (req_bfbdbux6 Lode 1/4)
WHAT: Apply the canonical Appendix A transform to the 13 Class-A *.schema.json files so they validate natively under Google response_json_schema, OpenAI strict:true, and Anthropic output_config json_schema. The transform strips $schema/$comment/length-numeric-items bounds, collapses oneOf, requires every property, and makes previously-optional properties nullable. meeting.box_2d and participation.participation_confidence are the only newly-required-nullable contract changes; this was pre-verified backward-compatible because consumers null/length-guard and journals validate on write only, so no migration is needed. The 3 dropped $comment notes are relocated as code/docstring comments.
GUARDS: Add tests/test_schema_strict_portability.py as the offline make ci guard, with a temporary PENDING_PORTABILITY allowlist covering the 4 Class-B schemas, chat, and build_rollup_schema(3). The guard also asserts allowlisted schemas still violate so the allowlist cannot rot. Add tests/integration/test_schema_provider_parity.py as an integration+requires_api raw-SDK strict parity test that skips cleanly without tests/fixtures/.env.
RATIFIED SCOPE DEVIATION (founder-approved during this session): scope §2/AC3 said tests/test_anthropic.py must be byte-unchanged, but its two sanitizes_describe_schema tests consumed the real describe.schema.json as a dirty-input fixture. Jer approved a minimal surgical edit: _load_describe_schema() now returns an inline dirty schema and the two tests plus that helper are the ONLY test_anthropic.py changes. All other test_anthropic.py tests and the test_sense_schema.py test_hydrate_runtime_enums_* block are byte-unchanged. The Anthropic sanitizer/forced-tool fallback in solstone/think/providers/ is intentionally unchanged because it remains load-bearing for Class B/C until Lodes 2-4.
SHIP OBLIGATION (AC7): the gated tests/integration/test_schema_provider_parity.py is run OUT-OF-BAND by VPE against the founder's solstone-scoped keys before ship and must pass 39/39 native (13x3, no fallback); that result is recorded in the ship/decision note. It was not run live in this lode because tests/fixtures/.env is absent.
SCOPE: this is Lode 1 of a founder-directed 4-lode program; req_bfbdbux6 closes when Lode 4 ships, not this commit. Lodes 2-4 remove their PENDING_PORTABILITY entries; Lode 4 deletes the allowlist mechanism and, after B/C/D land, the provider fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(cortex): let the orchestrator wait out a lost claim broadcast
Callosum is a best-effort broadcast bus. When a burst of logs.line traffic
caused the server to evict a slow consumer, cortex talent requests broadcast in
that window were simply lost. cortex_request's claim wait was a fixed 3 sends
about 1.0s apart, so a lost request exhausted the schedule in ~3s and raised
CortexNotClaimed. For the think orchestrator that became talent.fail
state=request_lost and the unit failed, to be re-walked hours later: 83 such
failures over 24h on a single-GPU local-model install, 67 of them on sense.
A delivered request claims in milliseconds, so the schedule exists to survive a
lost broadcast, not a slow one. Make it a per-call choice. cortex_request gains
a keyword-only claim_windows, where len(windows) is the total send count
including the initial broadcast and element i is the poll window after send i.
The default (1.0, 1.0, 1.0) reproduces today's ~3s fast-fail exactly, so
interactive callers -- convey chat, tools/sol, engage -- keep their latency and
take no diff. _dispatch_cortex_request, which fronts all seven thinking.py
spawn sites, opts into PATIENT_CLAIM_WINDOWS (1, 2, 4, 8, 15; ~30s budget).
Replace _CLAIM_WINDOW_S and _CLAIM_MAX_BROADCASTS with the two schedules; the
window walk makes the send count structurally equal to len(windows) rather than
implied by a max-broadcasts counter. Resolve the default at call time so tests
can monkeypatch it.
Widen the 49 cortex_request test mocks to accept the new keyword. The mocks
intercept think.cortex_request, so they must tolerate what the wrapper passes;
the alternative -- swallowing unknown kwargs in production -- would be a compat
shim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(concurrency): rebind stdlib shims onto product modules
Six tests across three files monkeypatched attributes on shared stdlib module objects (`time`, `threading`, `asyncio`), which is process-wide inside a pytest worker and reproduces the cross-test contamination mechanism d7767d145 set out to eliminate. The collaborator is now rebound on the product module under test (`plaud`, `operations`, `join_cli`), reusing the shim pattern already present in each file.
The plaud progress test previously consumed a 13-tick iterator to exact exhaustion via a patched stdlib `time.monotonic`, so any unrelated `monotonic()` call in the worker could steal a tick. It now uses a local `_ScriptedClock` bound to `plaud.time` that raises on over-read, making the exact call count an assertion rather than a coincidence of `range(13)`.
Test-only. No production behavior change; every owner-visible assertion preserved verbatim.
Verified: 8/8 focused; 20/20 serial single-process; 20/20 at 8 xdist workers; four break-to-red checks (plaud cadence `assert 0 == 2`; scout state `assert 'starting' == 'invited'`; scout terminal remap red on all three params; pairing taxonomy red on all three exact messages), each seam restored.
Known follow-up, deliberately out of scope: the same anti-pattern remains at solstone/apps/thinking/tests/test_confidential_routes.py:131,163,322 and tests/services/test_spl_handoff.py:657.
refactor(convey): remove web-UI password, login, and session auth
port 5015 is loopback-only, and mtls on 7657 is the sole network transport as of 0.6.0, so the web password gated nothing. remove the login form, flask session-cookie login, http basic auth, the trust_localhost localhost bypass, the journal password cli, and the convey.secret/app.secret_key flask-session machinery.
refactor the require_login gate to require_access while keeping private link revocation auth and the first-run /init onboarding redirect. there is no migration: orphan convey.password_hash, convey.secret, and trust_localhost keys in existing journal.json files are ignored, and _NEVER_TRANSFER_PATHS still scrubs password_hash and secret on export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
fix(providers): encode images for the receiving runtime's decoder
encode_image_part() chose its wire format from the source image format with no knowledge of what the receiving runtime could decode. WebP went to llama.cpp as image/webp, but mtmd decodes via stb_image, which has no WebP support, so the server silently dropped the image part and answered the text prompt alone. The model's refusal was then written into the journal as if it were a description. Field evidence: two of four import.image.vision calls at 39 input tokens, prompt plus chat template only with no image, and zero server error lines.
The target format is now a function of the receiving runtime's accepted media types via a new accepts keyword, defaulting to the narrow STB_IMAGE_MEDIA_TYPES so a call site that forgets to declare its runtime gets the universally decodable encoding. CLOUD_IMAGE_MEDIA_TYPES adds image/webp for the LiteLLM lanes.
One edit covers all three local-family lanes: bundled llama-server, BYO OpenAI-compatible endpoint, and hosted-confidential spp egress, because they all funnel through _image_content_part.
Payloads already in an accepted format are never re-encoded; accepted bytes stay byte-for-byte identical.
run_generate and run_agenerate no longer build messages on the bundled path before discarding them, so the image is encoded exactly once per bundled request.
New tests/test_provider_vision_parity.py pins the provider by format matrix.
feat(providers): resource-aware local-model guard + pre-activation surfacing
Replace the `.total` RAM gate for the bundled local provider with a shared
available-memory classifier and make it platform-aware at the point of choice
in journal Settings.
- New solstone/think/providers/memory.py: one available-memory classifier
(`assess_memory`) + derived MLX floor (12.5 GB resident + 0.5 GB headroom =
13 GB), disk free-bytes helper, and display-only total read. Detection
failure (psutil raise / nonsense) falls to warn-not-sufficient, never
silently sufficient.
- macOS (MLX): gate on available memory vs the derived floor and prevent
activation below it; keep the `insufficient RAM` blocked-reason prefix.
- Linux (llama): relax the hard block to a non-blocking `warning`; readiness,
start_bootstrap, ensure_artifacts_installed, and provider state no longer
refuse on low memory.
- Disk pre-flight (free vs artifact size) before any download in start_bootstrap.
- Settings availability payload adds `available_memory_gb`, `warning`, and
`download_bytes`; the local card surfaces requirement (floor + download size,
one VLM serves vision+thinking), detected memory, hosted=light vs
local=heavy/maximum-privacy framing, an experimental note, and a hosted-key
recovery path on a blocked Mac.
- Supervisor MLX gate uses the same available-memory logic as Settings.
- MLXModelSpec carries static artifact `size_bytes`; the stale `.total` min-RAM
floor field is removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(spp): harden GPU attestation leg and shared tarball extraction
Follow up on the self-audit of the NVIDIA GPU attestation leg.
Tar extraction now rejects symlink and hardlink members whose link target escapes the destination and passes filter=data as the stdlib backstop. Apply the guard to all three copies of _safe_extract_tarball in rfdetr, parakeet, and local because it is a security primitive; consolidating those copies remains deferred.
nvattest_install no longer trusts a partial or stale cache. _installed now requires a parsed sidecar whose archive_sha256 and version match the current pinned spec, so crash-interrupted or re-pinned installs re-verify instead of running an unrecorded binary. The sidecar temp path is also captured before writing so write failure cannot leak it.
classify_nvattest_result now requires a non-bool int returncode equal to 0, closing the same bool/int fail-closed gap already fixed for result_code.
The purity scanner now bans dangerous os calls, including system, popen, remove/unlink, rmdir, mkdir/makedirs, rename/replace, and the exec/spawn families, so allowing os.environ in binary.py cannot smuggle process execution or destructive writes into a pure file.
Remove dead code: drop the unused build_nvattest_version_command and reduce NvattestAcceptance to the claim field actually consumed. The overall-EAT veto still runs.
fix(providers): encode images for the receiving runtime's decoder
encode_image_part() chose its wire format from the source image format with no knowledge of what the receiving runtime could decode. WebP went to llama.cpp as image/webp, but mtmd decodes via stb_image, which has no WebP support, so the server silently dropped the image part and answered the text prompt alone. The model's refusal was then written into the journal as if it were a description. Field evidence: two of four import.image.vision calls at 39 input tokens, prompt plus chat template only with no image, and zero server error lines.
The target format is now a function of the receiving runtime's accepted media types via a new accepts keyword, defaulting to the narrow STB_IMAGE_MEDIA_TYPES so a call site that forgets to declare its runtime gets the universally decodable encoding. CLOUD_IMAGE_MEDIA_TYPES adds image/webp for the LiteLLM lanes.
One edit covers all three local-family lanes: bundled llama-server, BYO OpenAI-compatible endpoint, and hosted-confidential spp egress, because they all funnel through _image_content_part.
Payloads already in an accepted format are never re-encoded; accepted bytes stay byte-for-byte identical.
run_generate and run_agenerate no longer build messages on the bundled path before discarding them, so the image is encoded exactly once per bundled request.
New tests/test_provider_vision_parity.py pins the provider by format matrix.
test(backup): cover media offload pass
Cover the media offload pass with real temp-journal segments and mocked restic/supervisor seams. The tests pin precondition stalls before archive work, verification freshness and request behavior, dry-run zero side effects, disk-driven ledger crash recovery, per-segment halt semantics, and honest counters after partial progress.
Assert the load-bearing ordering directly: digest once from real content, archive and confirm every file, append the durable ledger witness before unlink, then write the raw_media_offload audit row. Existing backup tests also pin retry-lock argv placement, last_offload shape, maintenance discovery, and the new audit kind.
feat(push): self-provision reach relay token for push dispatch
Teach the journal to self-provision its push relay credential from the
hosted reach service using its durable link identity (home:{instance_id}
+ local CA), replacing the read-only PUSH_RELAY_SECRET env fallback and
the services.push.relay_token config string (clean break, no shim).
- link/ca.py: add mint_reach_assertion() — ES256 home-reach JWT
(aud=solstone-reach, scope=push.relay.enroll, 240s, no device_fp)
- think/push/reach.py: read_reach_token() pure reader +
ensure_reach_token() enroll/refresh client; POST
/reach/push/relay-token; structured services.push.reach_token state
(token/instance_id/expires_at/expires_epoch) written via
journal_config under hold_config_lock, lock held only around the RMW;
refresh on absent/expired/malformed/instance-mismatch/within-margin,
1h margin; never drops a still-valid token on a failed refresh
- portal_dispatch: devices-first, then ensure_reach_token(); send
Authorization: Bearer; env-normalize/pruning/short-circuit unchanged
- convey/push.py: register best-effort-enrolls; /status uses the pure
reader; /test self-heals via dispatch
- triggers: drop the relay-token pre-checks (token failures collapse to
portal_unavailable); never raise
- delete relay_auth.py; remove PUSH_RELAY_SECRET + relay_token paths
- link/paths.py: make LinkState.load() a truly pure read (no link/ dir
creation as a path-resolution side effect)
- secret hygiene: only status codes + exception type names are logged
- update docs/design/push.md (reach enrollment, state shape, wire
contract, corrected nudge reasons)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(push): self-provision reach relay token for push dispatch
Teach the journal to self-provision its push relay credential from the
hosted reach service using its durable link identity (home:{instance_id}
+ local CA), replacing the read-only PUSH_RELAY_SECRET env fallback and
the services.push.relay_token config string (clean break, no shim).
- link/ca.py: add mint_reach_assertion() — ES256 home-reach JWT
(aud=solstone-reach, scope=push.relay.enroll, 240s, no device_fp)
- think/push/reach.py: read_reach_token() pure reader +
ensure_reach_token() enroll/refresh client; POST
/reach/push/relay-token; structured services.push.reach_token state
(token/instance_id/expires_at/expires_epoch) written via
journal_config under hold_config_lock, lock held only around the RMW;
refresh on absent/expired/malformed/instance-mismatch/within-margin,
1h margin; never drops a still-valid token on a failed refresh
- portal_dispatch: devices-first, then ensure_reach_token(); send
Authorization: Bearer; env-normalize/pruning/short-circuit unchanged
- convey/push.py: register best-effort-enrolls; /status uses the pure
reader; /test self-heals via dispatch
- triggers: drop the relay-token pre-checks (token failures collapse to
portal_unavailable); never raise
- delete relay_auth.py; remove PUSH_RELAY_SECRET + relay_token paths
- link/paths.py: make LinkState.load() a truly pure read (no link/ dir
creation as a path-resolution side effect)
- secret hygiene: only status codes + exception type names are logged
- update docs/design/push.md (reach enrollment, state shape, wire
contract, corrected nudge reasons)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(push): self-provision reach relay token for push dispatch
Teach the journal to self-provision its push relay credential from the
hosted reach service using its durable link identity (home:{instance_id}
+ local CA), replacing the read-only PUSH_RELAY_SECRET env fallback and
the services.push.relay_token config string (clean break, no shim).
- link/ca.py: add mint_reach_assertion() — ES256 home-reach JWT
(aud=solstone-reach, scope=push.relay.enroll, 240s, no device_fp)
- think/push/reach.py: read_reach_token() pure reader +
ensure_reach_token() enroll/refresh client; POST
/reach/push/relay-token; structured services.push.reach_token state
(token/instance_id/expires_at/expires_epoch) written via
journal_config under hold_config_lock, lock held only around the RMW;
refresh on absent/expired/malformed/instance-mismatch/within-margin,
1h margin; never drops a still-valid token on a failed refresh
- portal_dispatch: devices-first, then ensure_reach_token(); send
Authorization: Bearer; env-normalize/pruning/short-circuit unchanged
- convey/push.py: register best-effort-enrolls; /status uses the pure
reader; /test self-heals via dispatch
- triggers: drop the relay-token pre-checks (token failures collapse to
portal_unavailable); never raise
- delete relay_auth.py; remove PUSH_RELAY_SECRET + relay_token paths
- link/paths.py: make LinkState.load() a truly pure read (no link/ dir
creation as a path-resolution side effect)
- secret hygiene: only status codes + exception type names are logged
- update docs/design/push.md (reach enrollment, state shape, wire
contract, corrected nudge reasons)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(push): backend chat-fold push trigger (content-free spn)
When a folded async-dispatch synthesis sol_message (B1's `origin`) is broadcast
on callosum, emit a content-free spn push via the dedup path so the answer finds
an owner who has left chat. Backend trigger only, a sibling to the two existing
push triggers. No iOS routing/badge (I3b), no on-device delivery (founder's spn
gate), no chat-engine change (B1 shipped the fold).
handle_chat_fold (triggers.py) fires only on event=="sol_message" with a
non-empty `origin` dict and falsy `requested_target` (the fold discriminator);
no-ops on dispatch acks, direct answers, and non-sol_message events. It
dispatches via dispatch_dedup_via_portal (id+action only) and never via
dispatch_via_portal (the content/summary leak path). Registered as the third
handler in runtime._on_callosum_message; the three handlers are mutually
exclusive on event.
Design decisions:
- Dedup/routing id = origin["logical_use_id"], NOT the fold's top-level use_id.
This corrects the scope's assumption that they are equal: the synthesis turn
carrying a folded answer gets a freshly-minted top-level use_id, while
origin["logical_use_id"] (the original dispatch turn) is stable across both
the live fold and a recovery-rebuilt fold of the same answer. Relay-side dedup
only works on the stable key, so the wire/dedup request_id is
origin["logical_use_id"]. This is the I3a->I3b contract.
- Fold push action = "chat_answer_ready" (module-level FOLD_PUSH_ACTION in
triggers.py, deliberately kept out of sol_initiated/copy.py's locked-literal
and JS-mirror machinery). This is the wire `action` value I3b/iOS keys on.
- Staleness-bounded viewing suppression: an unmatched owner_chat_open within
_VIEWING_STALENESS_MS (15 min) of the fold suppresses the push
(owner_viewing_chat); older unmatched opens are treated as not-viewing so a
lost owner_chat_dismissed (force-quit/crash/dropped SSE) never suppresses
forever. 15 min covers a continuous reading session that emits no fresh open
while self-healing quickly after a lost dismissed.
- Suppression is owner-global for v1 (presence reduces from the single journal's
chat stream; load_devices is owner-global) and reads only the fold's day (the
15-min window is short relative to a day; cross-midnight is an accepted v1
edge).
Nudge log: kind="chat_fold_push", dedupe_key=<logical_use_id>,
category=FOLD_PUSH_ACTION, seconds ts; skip reasons owner_viewing_chat,
no_relay_token, no_devices, portal_unavailable; dispatched/via="portal" on
success.
Tests: 10-case fold matrix in test_push_triggers.py (fires-when-not-viewing,
content-free recovery shape, ack/direct/non-sol_message no-ops, viewing
suppression, stale-open dispatch, dismissed-clears-suppression, origin-id dedup
invariant, gate skips); test_push_runtime.py updated to assert all three
handlers fire in order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(push): self-provision reach relay token for push dispatch
Teach the journal to self-provision its push relay credential from the
hosted reach service using its durable link identity (home:{instance_id}
+ local CA), replacing the read-only PUSH_RELAY_SECRET env fallback and
the services.push.relay_token config string (clean break, no shim).
- link/ca.py: add mint_reach_assertion() — ES256 home-reach JWT
(aud=solstone-reach, scope=push.relay.enroll, 240s, no device_fp)
- think/push/reach.py: read_reach_token() pure reader +
ensure_reach_token() enroll/refresh client; POST
/reach/push/relay-token; structured services.push.reach_token state
(token/instance_id/expires_at/expires_epoch) written via
journal_config under hold_config_lock, lock held only around the RMW;
refresh on absent/expired/malformed/instance-mismatch/within-margin,
1h margin; never drops a still-valid token on a failed refresh
- portal_dispatch: devices-first, then ensure_reach_token(); send
Authorization: Bearer; env-normalize/pruning/short-circuit unchanged
- convey/push.py: register best-effort-enrolls; /status uses the pure
reader; /test self-heals via dispatch
- triggers: drop the relay-token pre-checks (token failures collapse to
portal_unavailable); never raise
- delete relay_auth.py; remove PUSH_RELAY_SECRET + relay_token paths
- link/paths.py: make LinkState.load() a truly pure read (no link/ dir
creation as a path-resolution side effect)
- secret hygiene: only status codes + exception type names are logged
- update docs/design/push.md (reach enrollment, state shape, wire
contract, corrected nudge reasons)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(apps/reflections): retire mirror, redesign empty state, add /sample route
Per cpo/specs/in-flight/reflections-naming-and-empty-state.md:
- Sidebar icon 🪞 → 🌙.
- New empty state introduces the app, names sol as author, names the
journal as source, and shows a computed next-Sunday date (with a
graceful fallback). Populated index gains a persistent framing line,
a sample link, and a next-reflection footer.
- New /app/reflections/sample route renders the bundled fixture with
a "this is a sample" banner and no PDF button. Returns plain-text
404 when the fixture isn't on disk (packaged installs).
The cogitate (solstone/talent/weekly_reflection.md), route names,
talent name, YAML type, storage path, and detail view are unchanged.
Co-Authored-By: Codex <codex@openai.com>
fix(release): give the scrubbed build env hermetic zig cache paths
_scrubbed_build_env() passed only MATURIN_PEP517_ARGS, PATH, and PYTHONNOUSERSITE. The pinned Zig 0.16.0 invoked through maturin --zig resolves its cache directory with getAppDataDir and has no passwd fallback, so the first local candidate build step died with error: unable to resolve zig cache directory: AppDataDirUnavailable.
Give Zig explicit ZIG_GLOBAL_CACHE_DIR and ZIG_LOCAL_CACHE_DIR under the versionless, already-git-ignored target/release-zig-cache/ tree. The rail creates those directories, removes them in _default_clean_outputs pre-clean, and includes the cache root in cleanup_transients post-clean. PATH remains the only key sourced from the operator environment. Cache directory creation failures now raise DriverError with the rail's _failure shape instead of falling back to HOME, XDG, or cwd.
Do not synthesize HOME or XDG_CACHE_HOME. That would redirect uv, cargo/rustup, and python3 to a cold home, forcing network-dependent re-resolution: crates.io index plus 77 .crate downloads and a 113M cargo cache per candidate. That weakens --locked offline determinism to fix one tool; only Zig is actually broken.
Class audit: Zig is the only tool with no passwd fallback and the only one that fails loudly; zig env exits 1 under the old env and exits 0 with the cache keys. uv exits 0 and resolves the operator's home through the passwd DB, so it stays passwd-warm. cargo exits 0 as a rustup shim and resolves the operator's cargo/rustup homes through the passwd DB, so it stays passwd-warm. rustc exits 0 as a rustup shim and resolves the operator's rustup home through the passwd DB, so it stays passwd-warm. python3 exits 0 and resolves Path.home() through the passwd DB, so it stays passwd-warm. maturin owns no cache and is cwd-relative.
Add tests/test_release_candidate_driver_zig_env_contract.py. It runs real zig env with the exact dict _scrubbed_build_env() returns, asserts exit 0, and checks that the resolved global cache lands under the passed root. It falsifies the contract by stripping the ZIG_* keys and requiring a non-zero exit with AppDataDirUnavailable. It skips when Zig is absent, matching repo precedent. It does not compile in the unit lane: zig cc -v was measured exit 0 under the broken env and cannot falsify this defect. The gate enters make ci through make test.
Validation: make ci exited 0 with 14542 passed and 16 skipped. The real reproducer, uv build --package solstone-core under the constructed env, now exits 0 in about 38s. The AMBIENT_RELEASE_TOKEN no-leak assertion still holds, and the policy-before-build ordering assertion in tests/test_rust_policy_baseline.py still holds unchanged.
No other release rail semantics changed: advisory, channels, proofs, recovery, ledger, digests, the release.sh argv gate, and publication lockout are untouched.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
fix(release): validate macOS swift and notarytool evidence
make wheel-macos failed on the real macOS build host before any codesign or notarization work because the macOS lane compared synthesized pin labels against observed tool banners. Swift was pinned as Apple Swift 6.3.3 (...) while Apple prints Apple Swift version 6.3.3 (...), and the real host prefixes the first line with swift-driver version: 1.148.6. That dead gate existed in the shell signing helper, the preflight lane check, and native-record validation. notarytool was pinned as notarytool 1.1.2 (41), but xcrun notarytool --version prints the bare 1.1.2 (41); the old pin invented a tool-name prefix the tool never printed. This is not a version bump: 1.1.2 (41) stays unchanged.
The signing helper also wrote the pin constants themselves into signing_facts["tools"], making the native-record comparison unfalsifiable. It now records the observed line-1 Swift banner and the observed notarytool output, so the native records carry the same evidence the host produced.
Swift is now validated as host-variant evidence. One anchored regex parses both the pin and the observation, and only the (version, swiftlang, clang) identity triple is compared with ==. The swift-driver version: prefix is matched as host framing, so it does not defeat the gate and cannot smuggle a wrong version through substring or prefix matching. Swift joins HOST_VARIANT_TOOL_KEYS and inherits the non-string fail-closed guard at the untrusted build-host boundary. The shell helper and Python validators share the same grammar through the helper's existing python3 seam, with no parallel implementation.
| key | verdict |
|---|---|
| `python` | derived-token-not-a-banner — `Python 3.14.6` recorded as stripped `3.14.6` |
| `rustc` | host-invariant-retained-exact |
| `cargo` | host-invariant-retained-exact |
| `uv` | host-variant parser retained (fixed in 8841ffced), moved to the shared module |
| `maturin` | host-invariant-retained-exact |
| `cargo-deny` | host-invariant-retained-exact |
| `xcode` | derived-token-not-a-banner — line-membership on `Xcode 26.6` / `Build version 17F113` |
| `swift` | fixed |
| `codesign` | derived-token-not-a-banner — no `--version` flag exists; pinned-path check |
| `notarytool` | fixed |
| `signing_mode` | derived-token-not-a-banner — post-signing status constant |
Retained macOS evidence written under the old pin shape will fail --recover revalidation. This is intentional and there is no compatibility shim, matching the uv break accepted in 8841ffced.
The matcher dispatch is consolidated as one shared tool_value_matches_pin in release_tool_pins.py for both the preflight lane check and native-record validation. The dead MACOS_NOTARYTOOL_VERSION alias was also removed so the grounded notarytool output has one name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(release): accept the published tombstone record in retained recovery
Resolved the contradiction where `_validate_evidence_inventory` hard-rejected `CORE_UNSUPPORTED_TOMBSTONE_RECORD` as an unexpected third evidence entry while production publication required it, making production publication unreachable because recovery runs first in `publish_release`.
Acceptance is recovery-only. Candidate finalization and the post-promotion report stay strict, because the candidate rail writes only `ledger.json` and `proofs/*.json` and the record arrives from an external operator publish-and-verify step. The permission and the validating authority travel together as `publication_prerequisite_version`.
The record is validated verbatim by `validate_core_unsupported_tombstone_record` against the retained selector/ledger version; no schema, triples, or resolver constants were re-declared and no checkout metadata is read on the recovery path.
Candidate and bundle identity are unchanged: the record stays out of `candidate_digest`, `ledger_sha256`, `proof_sha256`, `bundle_digest`, `payload_files`, and upload classification; it surfaces only as a new `publication_prerequisite_inventory` report key.
The publisher production prerequisite check is untouched and still required.
The shared real-candidate fixture harness moved to `tests/helpers/release_candidate_fixtures.py` so the publisher integration units exercise the real recovery path without monkeypatching it.
Base-commit deviation: the task named `233801079b2786cf5de39a7abe6f0870351fae92` as the start point; work was done from current clean `origin/main` (`b1c8a6230`), which has `233081079` as an ancestor plus two unrelated commits (`453e284c9`, `b1c8a6230`).
No publication, network call, credential read, or retained-candidate mutation occurred.
Validation: focused release-candidate, release-publisher, transparency-publish, and rust-release-manifest suites pass; `make check-rust-release-manifest` and `make install-checks` pass; `make test` passes at 14845 passed / 16 skipped.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(packaging): cut release metadata to 1.0.15
Cut the release metadata to 1.0.15 from the root project version, then propagated derived packaging state through scripts/render_packaging.py and uv lock. The rendered leaf pyprojects, core workspace metadata, Cargo lock, uv.lock, and unsupported-platform tombstone all move together with no hand-edited derived files.
The stale-version coverage now asserts that live lock and workspace authorities equal the root project version derived at test time. It deliberately does not hardcode a release literal, so the next version cut updates the expected value by changing the root project version instead of rewriting the test.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
test(speakers): make the no-write proof xdist-safe
The old _tree_inventory oracle recorded (st_size, st_mtime_ns) for every entry, including directories. A sibling xdist worker writing tests/__pycache__/*.pyc inside the before-to-after window therefore produced two false positives: the new .pyc entry and a change to the tests/__pycache__ directory's own size and mtime.
Replace it with a non-following os.scandir inventory that prunes exactly five runtime cache basenames, and only when the entry is a real non-symlink directory with that exact name. Directories record identity, type, and permission mode only; they never record size, mtime, or ctime.
The exclusions are .git for VCS metadata directories, .venv for local dependency environments, .pytest_cache for pytest state, __pycache__ for Python bytecode, and .hypothesis because Hypothesis defaults its home to Path.cwd() / '.hypothesis'. The 6 unmarked default-selected tests in tests/test_openapi_schemathesis.py rewrite unicode_data/15.1.0/codec-utf-8.json.gz on every run.
Measured locally: inventory moved from 1.77s to 0.080s per pass and from 41,741 to 4,243 entries; .venv alone was 37,285 entries. The target test moved from 4.74s to 1.02s, and the speaker differential module moved from 6.43s to 1.26s.
The regression manufactures its own sibling with multiprocessing spawn, Barrier(2), and a queue, so it is deterministic under plain pytest and the 2-worker suite. The ordering proof is the barrier/queue happens-before chain, and the test fails rather than skips if the sibling does not participate.
Only test/oracle code changed. There are no production, release, packaging, or parallelism changes.
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
feat(indexer): local ONNX rerank scorer + pinned model install
Add a local CPU cross-encoder rerank capability for journal search in two
self-contained pieces; no caller is wired to it yet (follow-up lode).
Scorer (solstone/think/indexer/rerank_scorer.py):
- Lazy module-level singleton; onnxruntime/tokenizers/numpy imported only
inside the loader so the module stays light on the import path.
- Fail-closed: missing assets, load/inference failure, or an unexpected
output shape latch the scorer disabled for the process, log one warning,
and return None. Never raises; never touches the network.
- CPU InferenceSession (intra-op threads capped at min(8, cpu_count)),
512-token truncation, batches of 16, feeds only declared inputs.
Installer (solstone/think/providers/rerank_install.py + install-models wiring):
- Pinned Xenova/ms-marco-MiniLM-L-6-v2 ONNX + tokenizer at a fixed revision,
fetched into cache/providers/rerank/<revision>/ with a verified sidecar.
- Deliberate divergence from parakeet: size + sha256 are verified on the
.tmp file BEFORE the atomic rename, so no unverified file is ever exposed
at the final path; any failure cleans up leaving no partial install.
- journal install-models gains an unconditional rerank step after bundled
asset verification, honoring --check (verify-only, no network) and --force
(refetch); parakeet flow is byte-for-byte unchanged.
Also: tokenizers>=0.20 added to journal-host, onnx added to the dev group
(lock regenerated); Apache-2.0 notice for the rerank model; rerank_install
added to the journal-io raw-mechanic exclusion set alongside its peer
provider installers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(providers): bundled rf-detr.cpp install/verify/inspect path
Add solstone/think/providers/rfdetr_install.py to download, verify, and inspect the pinned rf-detr.cpp CLI engine binary plus GGUF model under cache/providers/rfdetr/, mirroring the rerank installer verify-before-rename download safety and the parakeet installer tar extraction guard and in-function host-fit gate. Every exposed path is sha256-verified before it lands: tarball, extracted inner binary, and model. Any failure cleans up and leaves no partial install. Exposes the pinned consumer API: ENGINE_REF, MODEL_NAME, install_rfdetr, check_rfdetr_model, rfdetr_paths 3-state accessor, and RfdetrInstallError.
Wire an unconditional rfdetr step into journal install-models between the rerank and parakeet steps, honoring --check as verify-only/no-network and --force as refetch with the same short-circuit contract. Non-x86_64-Linux hosts download nothing, write a platform-unavailable marker, and exit 0.
Add build_rfdetr_fit_report, THIRD_PARTY_NOTICES entries for the engine and model weights, and offline tests. rfdetr_install.py joins its sibling provider-cache modules in the journal-io-mechanic gate EXCLUDED_FILES because cache/providers/** is not journal data.
fix(release): move the public sol and solstone launchers to the root wheel
uv tool install exports only the root project's entry points and .data/scripts members to the user's bin dir, so the core-owned sol and solstone executables never landed on PATH even though the dependency wheel installed them into the tool environment.
The root wheel now ships two POSIX /bin/sh launchers that resolve their own installed directory, follow symlinks, and exec the sibling solstone-core with a private argv[1] identity token. solstone-core keeps exactly one compiled executable, and its indexer CLI remains byte-for-byte unchanged for absent, empty, or unknown identity tokens.
The compat console script is retired. Native compat delegation now execs the environment's sibling Python with -P -m solstone.think.sol_compat_cli while preserving the existing sentinel and argv0 marker behavior.
The ownership split is expressed once as ROOT_LAUNCHER_NAMES and CORE_SCRIPT_NAMES in scripts/check_wheel_contents.py, with release, smoke, macOS signing, access-clean, fixture, and integration consumers importing from that authority. CORE_SCRIPT_NAMES is narrowed in place to solstone-core, so core-only consumers kept the same authority name.
Validation: make ci exited 0 on the final settled tree containing both commits (14918 passed, 16 skipped), together with make test-integration (33 passed, no skips), make check-thin-base-install, python3 scripts/render_packaging.py --check, the focused release/packaging suites, and the static check targets. These commits were not independently gated.
Not run: no scripts/release.sh in any mode, no make publish-*, no make release*, nothing that builds or contacts a host, and no touch of the retained 1.0.14 candidate bytes or the orphaned production solstone-core-unsupported-platform==1.0.14 tombstone. dist/release-candidate/ and target/release-evidence/ remained absent.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(release): non-destructive, mirror-bound, signed-packet make audit
Replaces the GitHub-DB-refreshing make audit with a stable, non-destructive audit: verifies a minisign-signed freshness receipt against pinned sol trust (key ID + public-key SHA-256), materializes the RustSec advisory DB only from a locally-supplied git bundle at the receipt's signed commit, and runs the pinned cargo-deny advisory check fully offline against that isolated DB, emitting exactly one JSON success witness.
Four explicit operator inputs (AUDIT_ADVISORY_BUNDLE/RECEIPT/PUBKEY/LOCATOR); signature is derived as the adjacent <receipt>.minisig; locator is used only as cargo-deny's db-urls identity and never contacted; no GitHub fallback.
New authority module scripts/advisory_mirror_audit.py reuses the release-advisory isolation, minisign, and redaction primitives via import; the release-candidate prepare_policy_run path, PolicyRun/ledger schemas, and the 24h/14d rules are unchanged (regression-covered).
Recipe keeps the cargo-deny + minisign preflights, redirects preflight stdout to stderr, and suppresses command echo so make audit stdout is exactly the witness JSON and the private locator is never printed.
Adds a deterministic 113-test suite (fake tools/packets, real-git bundle materialization), rewrites the audit recipe contract test, and reconciles the now-false release_advisory_policy docstring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route mutation lanes through entity resolution boundary
Move the audited journal-writing name-resolution lanes onto record_entity_resolution so tier 5-8 matches persist ambiguity before any domain write and then reuse each lane's existing unresolved representation.
This prevents low-confidence names from creating duplicates, attaching speaker labels or voiceprints, or firing irreversible identity merges. In particular merge.py now stages ambiguous source entities, speakers.merge_names refuses before merge_entity(commit=True), and discovery exits before entity creation, voiceprint saves, labels, resolved-cluster writes, and candidate retroactive confirmation.
The import bridge checks resolved choices before the identical-content shortcut, so a staged low-confidence entity can be resolved and then applied on re-ingest without leaving received, id_map, or staging state incoherent.
feat(schemas): pre-send provider schema preparation, bounds ratchet, local eval harness
Foundation for the KG schema enrichment arc. Canonical *.schema.json files
may now carry maxItems/maxLength generation bounds; providers that cannot
accept a given keyword receive a deterministic, pre-send reduced copy. No
shipped schema is bounded in this lode — the framework is proven by fixture
and by the guardrail ratchet.
Entry point: solstone/think/schema_prep.py::prepare_provider_schema
Eval harness: make eval-schemas
D1 — per-provider support matrix (deviates from a blanket cloud strip).
Current provider docs say OpenAI and Google ACCEPT minItems/maxItems/
minimum/maximum; only minLength/maxLength are outside their supported
subset. Anthropic rejects numeric/string/array constraints beyond minItems
0/1, so all six bounds are stripped there. pattern is never stripped for any
provider. $schema/$comment are stripped for all three cloud providers. local
and any unregistered provider name receive a canonical deep copy; prep never
raises on an unknown provider. STRICT_UNSUPPORTED_KEYWORDS carries the doc
citations and is the single source of truth — the portability test imports it
rather than restating the list. Because today's 22 discovered generation
schemas carry zero bounds, every outgoing payload is byte-identical to before
this change; a parametrized test asserts that over all schemas x all
providers.
D2 — the transform drops only validation-only keywords, recursively. It is
pure, deep-copying, idempotent, and structure-preserving: required,
additionalProperties, type/nullability, enum, const, pattern, format, items
and descriptions are never touched. No runtime try-then-fallback was added;
c78f9ff5 deleted exactly that, and the reduction here is deterministic and
pre-send. local.py::_normalize_schema_patterns stays where it is as a
llama.cpp transport detail.
D4 — response validation stays CANONICAL. All three models.py call sites
(generate, generate_with_result, agenerate) hand the prepared schema to the
provider and keep validating the response against the canonical schema.
HAZARD for follow-on lodes: for anthropic, bounds are stripped from the
request but still enforced on the response, so once shipped schemas carry
maxItems/maxLength an over-long Anthropic response raises
SchemaValidationError. That is a deliberate fail-loudly choice (CLAUDE.md
§8), and it is covered by test_generate_validates_response_against_canonical_schema.
Also in this commit:
- tests/test_schema_strict_portability.py rewritten. violations() now checks
only the structural rules prep does not and must not fix (root object,
additionalProperties:false, all properties required, no oneOf). The
banned-key assertion moved to the provider-facing schema, per provider, so
maxItems legitimately survives for openai/google. Zero-facet hydration
guard retained (hydrate, then prep). Negative self-check strengthened.
- solstone/think/schema_bounds.py::unbounded_nodes — reusable deny-by-default
helper: arrays without maxItems, and free-text strings (no enum/const/
pattern/format) without maxLength.
- scripts/check_schema_bounds.py — ratchet wired into make install-checks,
following the check_call_http_only.py evaluate()->(new, stale, tracked)
idiom. 22 reasoned allowlist entries; stale entries that no longer violate
fail the gate so the allowlist cannot rot. daily_schedule.schema.json is
already clean and is deliberately absent. Each follow-on lode deletes its
entry.
- tests/eval_schemas.py + make eval-schemas — opt-in local strict-JSON eval
over tests/fixtures/schema_eval/cases.jsonl, including a deliberately
bounded case. Reports per-case schema-validity and content-preservation,
writes results atomically to the gitignored tmp/schema-eval/, and exits
non-zero naming `journal install-provider local` when the local model is
not ready. Not collected by pytest; scoring lives in
solstone/think/schema_eval.py with offline unit tests.
Existing test_local/test_models/test_anthropic/test_openai/test_google/
test_sense_schema/test_schedule_schema pass unmodified. No external network
call in any collected test; no new pytest marker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(schemas): pre-send provider schema preparation, bounds ratchet, local eval harness
Foundation for the KG schema enrichment arc. Canonical *.schema.json files
may now carry maxItems/maxLength generation bounds; providers that cannot
accept a given keyword receive a deterministic, pre-send reduced copy. No
shipped schema is bounded in this lode — the framework is proven by fixture
and by the guardrail ratchet.
Entry point: solstone/think/schema_prep.py::prepare_provider_schema
Eval harness: make eval-schemas
D1 — per-provider support matrix (deviates from a blanket cloud strip).
Current provider docs say OpenAI and Google ACCEPT minItems/maxItems/
minimum/maximum; only minLength/maxLength are outside their supported
subset. Anthropic rejects numeric/string/array constraints beyond minItems
0/1, so all six bounds are stripped there. pattern is never stripped for any
provider. $schema/$comment are stripped for all three cloud providers. local
and any unregistered provider name receive a canonical deep copy; prep never
raises on an unknown provider. STRICT_UNSUPPORTED_KEYWORDS carries the doc
citations and is the single source of truth — the portability test imports it
rather than restating the list. Because today's 22 discovered generation
schemas carry zero bounds, every outgoing payload is byte-identical to before
this change; a parametrized test asserts that over all schemas x all
providers.
D2 — the transform drops only validation-only keywords, recursively. It is
pure, deep-copying, idempotent, and structure-preserving: required,
additionalProperties, type/nullability, enum, const, pattern, format, items
and descriptions are never touched. No runtime try-then-fallback was added;
c78f9ff5 deleted exactly that, and the reduction here is deterministic and
pre-send. local.py::_normalize_schema_patterns stays where it is as a
llama.cpp transport detail.
D4 — response validation stays CANONICAL. All three models.py call sites
(generate, generate_with_result, agenerate) hand the prepared schema to the
provider and keep validating the response against the canonical schema.
HAZARD for follow-on lodes: for anthropic, bounds are stripped from the
request but still enforced on the response, so once shipped schemas carry
maxItems/maxLength an over-long Anthropic response raises
SchemaValidationError. That is a deliberate fail-loudly choice (CLAUDE.md
§8), and it is covered by test_generate_validates_response_against_canonical_schema.
Also in this commit:
- tests/test_schema_strict_portability.py rewritten. violations() now checks
only the structural rules prep does not and must not fix (root object,
additionalProperties:false, all properties required, no oneOf). The
banned-key assertion moved to the provider-facing schema, per provider, so
maxItems legitimately survives for openai/google. Zero-facet hydration
guard retained (hydrate, then prep). Negative self-check strengthened.
- solstone/think/schema_bounds.py::unbounded_nodes — reusable deny-by-default
helper: arrays without maxItems, and free-text strings (no enum/const/
pattern/format) without maxLength.
- scripts/check_schema_bounds.py — ratchet wired into make install-checks,
following the check_call_http_only.py evaluate()->(new, stale, tracked)
idiom. 22 reasoned allowlist entries; stale entries that no longer violate
fail the gate so the allowlist cannot rot. daily_schedule.schema.json is
already clean and is deliberately absent. Each follow-on lode deletes its
entry.
- tests/eval_schemas.py + make eval-schemas — opt-in local strict-JSON eval
over tests/fixtures/schema_eval/cases.jsonl, including a deliberately
bounded case. Reports per-case schema-validity and content-preservation,
writes results atomically to the gitignored tmp/schema-eval/, and exits
non-zero naming `journal install-provider local` when the local model is
not ready. Not collected by pytest; scoring lives in
solstone/think/schema_eval.py with offline unit tests.
Existing test_local/test_models/test_anthropic/test_openai/test_google/
test_sense_schema/test_schedule_schema pass unmodified. No external network
call in any collected test; no new pytest marker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(think): strip maxItems for the google provider
On the google BYO lane, litellm passes our JSON Schema verbatim as Gemini's responseJsonSchema. Gemini compiles maxItems into a bounded decoding grammar with an undocumented, additive, whole-schema budget; over budget it returns a bare 400 INVALID_ARGUMENT naming no keyword, path, or limit. 7 of the 8 shipped schemas carrying maxItems are rejected as-is; all 8 pass stripped. Because sense is the floor talent (schedule: segment, priority: 5), its 400 meant no segment talent ran at all on google while brain health still read "sol can think".
The root cause is that the whole-schema grammar budget is additive across every bounded array and scales with N x item complexity, not a per-keyword or per-array limit. Tuning individual maxItems values is therefore not the fix, no schema value was changed, and minItems is free.
The change adds one keyword to the google row of STRICT_UNSUPPORTED_KEYWORDS, corrects the provider comments, and updates the tests that previously asserted the broken behavior.
Residual risk: stripping maxItems from the google request means Gemini is no longer grammar-constrained at generation time, while models.py still validates the response against the canonical schema and x-truncate has no array path. The failure mode changes shape rather than disappearing: before, 400 INVALID_ARGUMENT on every segment, always; after, a segment dense enough to produce more than 96 entities raises SchemaValidationError instead of being grammar-clamped. That is a strictly better trade, a loud, specific, occasional failure replacing a silent, total one, and it is the posture Anthropic already ships under.
Second-order: this makes the entire segment talent chain reachable on the google lane for the first time. Entities, facets, screen, speaker attribution, and segment summaries have never executed there.
feat(schemas): pre-send provider schema preparation, bounds ratchet, local eval harness
Foundation for the KG schema enrichment arc. Canonical *.schema.json files
may now carry maxItems/maxLength generation bounds; providers that cannot
accept a given keyword receive a deterministic, pre-send reduced copy. No
shipped schema is bounded in this lode — the framework is proven by fixture
and by the guardrail ratchet.
Entry point: solstone/think/schema_prep.py::prepare_provider_schema
Eval harness: make eval-schemas
D1 — per-provider support matrix (deviates from a blanket cloud strip).
Current provider docs say OpenAI and Google ACCEPT minItems/maxItems/
minimum/maximum; only minLength/maxLength are outside their supported
subset. Anthropic rejects numeric/string/array constraints beyond minItems
0/1, so all six bounds are stripped there. pattern is never stripped for any
provider. $schema/$comment are stripped for all three cloud providers. local
and any unregistered provider name receive a canonical deep copy; prep never
raises on an unknown provider. STRICT_UNSUPPORTED_KEYWORDS carries the doc
citations and is the single source of truth — the portability test imports it
rather than restating the list. Because today's 22 discovered generation
schemas carry zero bounds, every outgoing payload is byte-identical to before
this change; a parametrized test asserts that over all schemas x all
providers.
D2 — the transform drops only validation-only keywords, recursively. It is
pure, deep-copying, idempotent, and structure-preserving: required,
additionalProperties, type/nullability, enum, const, pattern, format, items
and descriptions are never touched. No runtime try-then-fallback was added;
c78f9ff5 deleted exactly that, and the reduction here is deterministic and
pre-send. local.py::_normalize_schema_patterns stays where it is as a
llama.cpp transport detail.
D4 — response validation stays CANONICAL. All three models.py call sites
(generate, generate_with_result, agenerate) hand the prepared schema to the
provider and keep validating the response against the canonical schema.
HAZARD for follow-on lodes: for anthropic, bounds are stripped from the
request but still enforced on the response, so once shipped schemas carry
maxItems/maxLength an over-long Anthropic response raises
SchemaValidationError. That is a deliberate fail-loudly choice (CLAUDE.md
§8), and it is covered by test_generate_validates_response_against_canonical_schema.
Also in this commit:
- tests/test_schema_strict_portability.py rewritten. violations() now checks
only the structural rules prep does not and must not fix (root object,
additionalProperties:false, all properties required, no oneOf). The
banned-key assertion moved to the provider-facing schema, per provider, so
maxItems legitimately survives for openai/google. Zero-facet hydration
guard retained (hydrate, then prep). Negative self-check strengthened.
- solstone/think/schema_bounds.py::unbounded_nodes — reusable deny-by-default
helper: arrays without maxItems, and free-text strings (no enum/const/
pattern/format) without maxLength.
- scripts/check_schema_bounds.py — ratchet wired into make install-checks,
following the check_call_http_only.py evaluate()->(new, stale, tracked)
idiom. 22 reasoned allowlist entries; stale entries that no longer violate
fail the gate so the allowlist cannot rot. daily_schedule.schema.json is
already clean and is deliberately absent. Each follow-on lode deletes its
entry.
- tests/eval_schemas.py + make eval-schemas — opt-in local strict-JSON eval
over tests/fixtures/schema_eval/cases.jsonl, including a deliberately
bounded case. Reports per-case schema-validity and content-preservation,
writes results atomically to the gitignored tmp/schema-eval/, and exits
non-zero naming `journal install-provider local` when the local model is
not ready. Not collected by pytest; scoring lives in
solstone/think/schema_eval.py with offline unit tests.
Existing test_local/test_models/test_anthropic/test_openai/test_google/
test_sense_schema/test_schedule_schema pass unmodified. No external network
call in any collected test; no new pytest marker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: retire graph app + home network-pulse + entities strength/intelligence (KG lode 1/3)
The producer side of the legacy KG pipeline was retired on 2026-04-18
(9a5d800b, 930f1d36, 53762ba9), leaving the entities and entity_signals
SQLite tables silently empty on fresh installs. This lode removes the
consumer surfaces that were already silently broken:
* delete solstone/apps/graph/ (Convey app + its API + workspace template)
* strip the Network Pulse section from the home app (routes.py
_collect_entities_*, _build_pulse_context entities/network_summary,
workspace.html template block + CSS + section registry, orphaned
_format_entity_summary/_format_entity_count/_ENTITY_TYPE_LABELS,
welcome-paragraph "people in your network" phrase)
* delete `sol call entities strength` and `sol call entities intelligence`
CLI verbs and their indexer backends (get_entity_strength,
get_entity_intelligence, _compute_strength_scores, _strength_score,
_recency_decay in solstone/think/indexer/journal.py + the
__init__.py re-exports)
* refactor solstone/think/awareness.py:compute_thickness off
get_entity_strength to two new file-direct readers in
solstone/think/entities/observations.py (count_entities_with_min_-
observation_depth, iter_entity_names_for_recall). Return shape and
composite ready logic unchanged.
* update talent prompts that called the deleted CLI verbs
(morning_briefing.md, partner.md, solstone/SKILL.md,
apps/entities/talent/entities/SKILL.md). Substitutions are deletions,
not fake equivalents (per AGENTS.md §8 — no shims, and analogous for
prompts).
* delete tests that exclusively exercised the removed surface
(TestEntityStrength, TestEntityIntelligence, TestEntityIntelligenceBrief,
photos test_strength_includes_photo_count). Rename
tests/test_entity_intelligence.py → tests/test_search_entities.py
for the surviving search coverage.
* rewire tests/test_awareness.py::TestComputeThickness patches to the
new observation-direct helpers.
* remove graph verifier entries and baselines (tests/verify_api.py,
tests/verify_browser.py, tests/baselines/api/graph/,
tests/baselines/visual/graph/).
Lodes 2 and 3 will drop the photos→entity_signals integration and the
entities/entity_signals table writers + CREATE statements respectively.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(browser): treat {host}.browser as a first-class journal modality
Make the journal validate, render, and index the solstone-browser
extension's per-site browser_<host>.jsonl uploads, and unblock
browser-only segments in the pipeline.
- Contract: add browser.schema.json (format_id browser-jsonl,
file_kind headered_jsonl) with an identical header/record row union
(required floor t/ts only, ctx optional, additionalProperties true)
so multi-ctx, legacy ctx-less, and delta-first files all validate;
route browser_*.jsonl in schema_for_filename() so ingest gates it.
- Formatter: add solstone/think/browser_formatter.py — format_browser()
(one snapshot chunk per segment_start, add/update delta chunks,
remove skipped, epoch-ms timestamps, agent="browser") registered
indexed=True, plus format_browser_text(). Output is bounded per file
to the shared 32k extraction budget with an explicit truncation marker.
- Visibility (atomic): detect browser data-state (analyzed on presence),
add "browser" to the scan/cluster modality set, and add "browser" to
SEGMENT_NO_PROCESSING_MODALITIES so browser-only segments are visible
and yield zero completion blockers.
- Consistency: browser_*.jsonl counts as segment content in the verify
CLI; layout.json accepts the browser producer path; bundle regenerated.
- Fixtures + tests cover formatter shape, degenerate/oversized cases,
contract validation, data-state/scan visibility, zero-blocker
completion, and stream/agent-tagged indexing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(convey): make the notification icon a Lucide name
The notification icon contract is now a Lucide icon name, not an emoji. Emitters in JS, Python, and sol notify help send names; the renderer resolves names to vouched SVG and never renders the caller's value as markup. A legacy glyph-to-name table keeps already-persisted localStorage['solstone:notification_history'] entries rendering correctly; that is a migration path for external data, not a backwards-compatibility shim.
The honest price is that this route deliberately manufactures window.ConveyIcons.svg('name') literals inside the notifications object whose only consumer is the guard test's allow_list <= used direction. It trades a one-line regex change for a hand-maintained duplicate list that must stay in sync with CONVEY_ICON_NAMES, and it narrows the notification namespace from 36 available names to 13. It is the right call here because the safety property - a caller can never inject markup through icon - is worth it. It is not parity with L3's genuinely-free zero-mechanism route, and a later session must not read it as precedent for always manufacturing literals.
D5 and D6 were fixed, not deferred. ERROR_ICON in SurfaceState was a hand-rolled inline triangle-alert SVG; it now uses window.ConveyIcons.svg('triangle-alert'). _updateCard never refreshed the icon even though show() updates existing.icon for keyed notifications - invisible while both were the same emoji, but a stale icon under resolved SVG. It now refreshes from the resolver.
The browser Notification icon member was dropped because it is specified as a URL; the old emoji was parsed as a relative URL and fetched, which always failed. Title, body, and tag still deliver.
feat(think): rebuild pulse as a lite generate on cadence + retire awareness_tender
Rebuild pulse from a cogitate/schedule:segment tool-loop into a lite
generate/schedule:cadence talent — the structural twin of steward. Pulse is
now a standing, always-current artifact persisted to the day-jsonl accumulator
(chronicle/<day>/talents/pulse.jsonl), serving the home glance and chat
situational awareness (incl. mobile cold-open).
- New accumulate:true frontmatter flag: _apply_output_persistence skips the
single-file write so a generate talent persists only via its post-hook's
append_record (output:json + schema stay declared; the JSON schema still
reaches the model). Reusable for steward's future migration; covers the
schedules that route through _apply_output_persistence (cadence + daily/weekly).
- pulse.{md,py,schema.json}: closed 4-field schema (title, one_sentence,
full_details, needs_you); pre-hook gathers context in-process (previous
record, completed-since window, awareness/anticipated/entities/partner —
folding in the deleted awareness_tender's deterministic reads) and degrades
gaps without crashing; post-hook normalizes to the closed contract and
appends the record.
- Readers migrated onto read_latest: home (strict today-gate, lookback_days=0),
chat ($situational, default lookback for cold-open), morning briefing.
- Fix _load_flow_md: resolve via day_path(today) (was missing chronicle/);
documented that flow.md is no longer produced by any talent.
- Delete awareness_tender, the journal identity pulse --write / identity
awareness verbs, the awareness.md identity-template entry, the pulse/
awareness_tender segment-dispatch blocks, NEVER_SKIP_DAILY, and the now-
vestigial recommend.pulse_update (sense.md + sense.schema.json + consumers).
- Tests/baselines/docs updated to the new inventory; new coverage for the
pulse hooks, the home/chat readers, the accumulate no-single-file guarantee,
and the agent=pulse search round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(setup): invoke journal-access steps through sol, not the journal surface
setup built skills_user, skills_journal, and brain_bootstrap as
sys.executable -m solstone.think.sol_cli <cmd>. That module entry is
journal_main, i.e. always the journal surface, and since 4f0d20722 added the
generated access-command rejection the dispatcher exits 2 for skills and call.
So journal setup failed two of its own steps and exited non-zero on every run.
On an installed host that was merely loud: the service still came up. In the
macOS app first-run onboarding it is fatal. The ritual waits for a setup that
reports completion, setup never writes completed_at, and the install hangs.
Observed on ja1r as five paired-profile lanes stalling at setup_step brain for
600s with skills_user and skills_journal failed in setup-state.json. It is a
1.0.16 regression: 4f0d20722 is in v1.0.16 and not v1.0.13, which is why the
7/24 macOS gate passed on the same setup.py.
Route all three through a new sol_console_command, mirroring the existing
journal_console_command precedent. sol ships beside the interpreter in the same
bin dir and owns the access surface.
The existing expected_* helpers asserted the broken argv verbatim, so they
green-lit the regression; they now assert the sol form. Added an invariant test
anchored to the generated JOURNAL_ACCESS_ONLY_COMMANDS inventory, so a newly
added access command cannot silently reintroduce this. It fails against the old
form. Note the 61 rc==0 assertions in this suite all patch subprocess, so they
never execute the dispatch and could not have caught it.
refactor(steward): demote health pass to report-only; fix stale-marker reprocess deadlock
The deterministic steward health pass no longer auto-fires per-segment
reprocesses. `run_recipe_pass` becomes report-only (zero POSTs, zero
recipe.outcome rows; keeps its three empty-list keys for the heartbeat
pass event). Genuinely-pending segments are already re-run by the daily
sensing pre-phase, so firing remediated nothing the pipeline doesn't
handle — it only widened latency, at the cost of escalation/re-verify
bookkeeping that existed solely to track fired repairs.
Removed: the fire/detect/re-verify/escalation machinery and every
escalated_targets render branch; the unused for_range health_report
gather; the always-empty `## Trends (last 7d)` section (constant,
renderer, validator, bootstrap default, fixture); and the unmapped
reprocess_stale suggested-action (Python, talent prompt, JSON schema).
Fixed the now-sole manual reprocess path's deadlock: a stale
`.analyzing_<modality>` marker classified FAILED was never cleared, so
create_analyzing_marker hit FileExistsError and never respawned. The
FAILED branch now calls repair_modality_markers (reusing
ANALYZING_STALE_SECONDS) to rename the stale marker before the unlink,
so reprocess respawns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(talent): audit fixes for the JSON morning briefing
render_briefing_markdown no longer borrows talent_outputs.ABSENT_TEXT, whose "Not specified in this document" copy is wrong for a briefing and reaches the owner through sol call ... briefing as well as the search index. Empty sections now render a briefing-specific "Nothing to report.", and an absent coverage_preamble omits the blockquote entirely rather than printing filler, matching render_briefing_sections' omit-empty behavior. All five ## headings still render. _clean stays duplicated across briefing.py and talent_outputs.py on purpose: importing an underscore-prefixed private name across module boundaries is worse than a one-line duplicate.
briefing_cmd's newest-first fallback dropped its exists() / st_size > 0 precheck, which was dead once load_briefing began validating shape, and stopped shadowing the name briefing between a Path in the loop and a dict in the --day branch. morning_briefing_path is no longer imported there. A new test covers a malformed briefing at the newest day falling through to an older valid one, behavior that is now load-driven rather than size-driven.
tests/eval_schemas.py::load_cases runs schema_path cases through hydrate_runtime_enums, but make ci never invokes that harness, so the call was uncovered. A unit test now asserts the morning_briefing case's reading.items.properties.facet carries no __RUNTIME_FACETS__ sentinel after loading, either replaced with live facet slugs or with the enum key dropped on a zero-facet journal.
The stats/stats API-baseline diff reported during audit was not a regression and no baseline changed. make verify-api at ce4409b7 reported ten failing endpoints against nine at base dbd036e7. The extra one was traced to a stale tests/fixtures/journal/stats.json runtime artifact left behind by an earlier sandbox run: git-ignored, but read by the sandbox as source. Removing it makes the HEAD failure set exactly equal to the base failure set: config/convey, sol/run-detail, sol/stats-month, sol/badge-count, search/search, thinking/providers, tokens/usage, tokens/stats-month, tokens/daily. Those nine are pre-existing drift on clean main and are out of scope here. make update-api-baselines was deliberately not used to fix this commit, because it rewrites every baseline and would have laundered that pre-existing drift into this commit. The two baselines updated in ce4409b7 were already correct.
The Home briefing card was observed end-to-end against a seeded sandbox journal: briefing_phase: active, briefing_summary: "Morning briefing — 3 meetings, 2 items need attention", briefing_needs_badge: "1 item also in Pulse needs", briefing_needs_deduped an array of two strings, and briefing_sections five markdown bodies. No browser screenshot was taken because Playwright is not installed on this machine (ModuleNotFoundError: No module named 'playwright'); the API payloads above are the wire contract home.js consumes, and no visual pass is claimed.
Declined during audit: _briefing_summary keeps both its briefing and sections arguments because the structured meeting count and the first-non-empty-line fallback genuinely need each. _briefing_freshness's second path resolution stays because _briefing_output_format() is lru_cached and a request-scoped context refactor is not worth it. No defensive handling was added around get_journal() raising SolstoneNotConfigured on an unwritable journal, per CLAUDE.md §8; that failure mode is identical for every get_journal() caller. The narrative frontmatter prose in docs/design/yesterdays-processing-card.md and docs/design/voice-server.md remains a follow-up, as ce4409b7 already recorded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(steward): demote health pass to report-only; fix stale-marker reprocess deadlock
The deterministic steward health pass no longer auto-fires per-segment
reprocesses. `run_recipe_pass` becomes report-only (zero POSTs, zero
recipe.outcome rows; keeps its three empty-list keys for the heartbeat
pass event). Genuinely-pending segments are already re-run by the daily
sensing pre-phase, so firing remediated nothing the pipeline doesn't
handle — it only widened latency, at the cost of escalation/re-verify
bookkeeping that existed solely to track fired repairs.
Removed: the fire/detect/re-verify/escalation machinery and every
escalated_targets render branch; the unused for_range health_report
gather; the always-empty `## Trends (last 7d)` section (constant,
renderer, validator, bootstrap default, fixture); and the unmapped
reprocess_stale suggested-action (Python, talent prompt, JSON schema).
Fixed the now-sole manual reprocess path's deadlock: a stale
`.analyzing_<modality>` marker classified FAILED was never cleared, so
create_analyzing_marker hit FileExistsError and never respawned. The
FAILED branch now calls repair_modality_markers (reusing
ANALYZING_STALE_SECONDS) to rename the stale marker before the unlink,
so reprocess respawns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(think): rebuild pulse as a lite generate on cadence + retire awareness_tender
Rebuild pulse from a cogitate/schedule:segment tool-loop into a lite
generate/schedule:cadence talent — the structural twin of steward. Pulse is
now a standing, always-current artifact persisted to the day-jsonl accumulator
(chronicle/<day>/talents/pulse.jsonl), serving the home glance and chat
situational awareness (incl. mobile cold-open).
- New accumulate:true frontmatter flag: _apply_output_persistence skips the
single-file write so a generate talent persists only via its post-hook's
append_record (output:json + schema stay declared; the JSON schema still
reaches the model). Reusable for steward's future migration; covers the
schedules that route through _apply_output_persistence (cadence + daily/weekly).
- pulse.{md,py,schema.json}: closed 4-field schema (title, one_sentence,
full_details, needs_you); pre-hook gathers context in-process (previous
record, completed-since window, awareness/anticipated/entities/partner —
folding in the deleted awareness_tender's deterministic reads) and degrades
gaps without crashing; post-hook normalizes to the closed contract and
appends the record.
- Readers migrated onto read_latest: home (strict today-gate, lookback_days=0),
chat ($situational, default lookback for cold-open), morning briefing.
- Fix _load_flow_md: resolve via day_path(today) (was missing chronicle/);
documented that flow.md is no longer produced by any talent.
- Delete awareness_tender, the journal identity pulse --write / identity
awareness verbs, the awareness.md identity-template entry, the pulse/
awareness_tender segment-dispatch blocks, NEVER_SKIP_DAILY, and the now-
vestigial recommend.pulse_update (sense.md + sense.schema.json + consumers).
- Tests/baselines/docs updated to the new inventory; new coverage for the
pulse hooks, the home/chat readers, the accumulate no-single-file guarantee,
and the agent=pulse search round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(sol): remove Python sol skills implementation
Relocate _discover_project_sources and _ROUTER_SKILL_NAMES into solstone/think/doctor.py rather than sharing them, because doctor is still Python and must not shell out to the native binary. They are knowingly duplicated with core/crates/solstone-core-sol/src/skills.rs, and that duplication ends when doctor itself is ported.
Exactly one assertion is knowingly dropped for a reason other than the build verb removal: the duplicate-router-name ValueError branch from the original skills_cli.discover_project_sources. It is unreachable now that the required payload names are a declared constant of two distinct literals.
The build verb is removed from the CLI surface as a deliberate, operator-approved change. Its sys.path.insert plus from scripts import build_skill_references scaffolding is deleted outright, not ported. make skills invokes scripts/build_skill_references.py directly as of the preceding lode.
Delete scripts/capture_skills_parity_oracle.py in this same commit. The harness drives solstone.think.skills_cli directly: it subprocesses python -m solstone.think.skills_cli and monkeypatches skills_cli.get_project_root and skills_cli.resources.files. Once that module is gone, the script cannot run at all; it would be dead code, not merely stale. Its provenance role is fully preserved: the harness and the 30 vectors it produced are committed together at 5240cfaaf, strictly before this deletion, so git log over core/fixtures/native-sol/skills-parity-v1/ still proves the vectors were captured from the live Python oracle. The frozen vectors themselves remain in the tree and continue to gate the native implementation.
Leave solstone/think/setup.py deliberately untouched. Its skills_user_command() and skills_journal_command() build python -m solstone.think.sol_cli skills install ..., but skills is in JOURNAL_ACCESS_ONLY_COMMANDS, so sol_cli rejects it with exit 2 and journal setup reports both skills_user and skills_journal as failures. That defect predates this lode and is left for a follow-up; tests/test_setup.py and tests/test_setup_jsonl.py pin the current argv shape and pass unmodified.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Route mutation lanes through entity resolution boundary
Move the audited journal-writing name-resolution lanes onto record_entity_resolution so tier 5-8 matches persist ambiguity before any domain write and then reuse each lane's existing unresolved representation.
This prevents low-confidence names from creating duplicates, attaching speaker labels or voiceprints, or firing irreversible identity merges. In particular merge.py now stages ambiguous source entities, speakers.merge_names refuses before merge_entity(commit=True), and discovery exits before entity creation, voiceprint saves, labels, resolved-cluster writes, and candidate retroactive confirmation.
The import bridge checks resolved choices before the identical-content shortcut, so a staged low-confidence entity can be resolved and then applied on re-ingest without leaving received, id_map, or staging state incoherent.
feat(schemas): wrap 4 Class-B root-array schemas + permanent consumer unwrap (req_bfbdbux6 Lode 2/4)
WHAT: Wrap extract, detect_transcript_segment, speaker_attribution, and schedule root arrays in strict-portable one-key objects (frame_ids, segments, attributions, events). Apply the strict-portable transform at every nesting depth: strip banned schema keywords, require all object properties, set additionalProperties:false, and preserve enums, patterns, and nullable unions.
CONSUMERS: Update the four single parse-sites to unwrap the wrapper key while permanently tolerating bare-list outputs as a defensive shape-compat shim. Relocate the detect_transcript_segment schema $comment to the _SEGMENT_SCHEMA load-site code comment.
GUARDS: Promote the four Class-B ids out of PENDING_PORTABILITY. Rename the provider parity list/test generically and extend it to 17 schemas x 3 providers = 51 cases. Reconcile schema-shape and parse-site tests, including dropping the three stripped-constraint negatives by design.
SHIP NOTE: Consumer-completeness sweep found 4 single boundaries and no additional raw-list parse sites. No live path was found by which a stale cached bare-list output file is fed into schedule.post_process() or speaker_attribution.post_process(); the permanent bare-list tolerance is a defensive compatibility shim mandated by scope, not justified by a discoverable stale-cache replay path.
CI: make ci green (4922 passed, 11 skipped, 8 deselected, 1 unrelated warning).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(speakers): harden identify ledger remediation
Fold existing identify operations before planning so retried request ids replay or conflict instead of returning stale planning errors, and resume uses the stored prepared plan. Add the folded undoing state so interrupted undo work is never replayed as the original success.
Make the retro tracker phase fail loudly on voiceprint save errors so incomplete work is not checkpointed. Emit durable undo_repair_required records for structural undo failures, and restore the stable response contract fields retro_voiceprints_saved, operation_state, and corrections_appended.
Map recoverable, repair, conflict, not-found, in-progress, and undoing outcomes to non-2xx route envelopes. Keep store read paths write-free, extend dismissal filtering to the speakers status cluster surface, fail closed on absent or unreadable edge state while scanning edge sources, and resurface keep-separate curation rows once the assertion is tombstoned or the watermark is exceeded.
Remove the dead resolved-cluster writer, correct the lock-order docs, and reject duplicate reviewed near-match ids explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(speakers): harden identify ledger remediation
Fold existing identify operations before planning so retried request ids replay or conflict instead of returning stale planning errors, and resume uses the stored prepared plan. Add the folded undoing state so interrupted undo work is never replayed as the original success.
Make the retro tracker phase fail loudly on voiceprint save errors so incomplete work is not checkpointed. Emit durable undo_repair_required records for structural undo failures, and restore the stable response contract fields retro_voiceprints_saved, operation_state, and corrections_appended.
Map recoverable, repair, conflict, not-found, in-progress, and undoing outcomes to non-2xx route envelopes. Keep store read paths write-free, extend dismissal filtering to the speakers status cluster surface, fail closed on absent or unreadable edge state while scanning edge sources, and resurface keep-separate curation rows once the assertion is tombstoned or the watermark is exceeded.
Remove the dead resolved-cluster writer, correct the lock-order docs, and reject duplicate reviewed near-match ids explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(speakers): harden identify ledger remediation
Fold existing identify operations before planning so retried request ids replay or conflict instead of returning stale planning errors, and resume uses the stored prepared plan. Add the folded undoing state so interrupted undo work is never replayed as the original success.
Make the retro tracker phase fail loudly on voiceprint save errors so incomplete work is not checkpointed. Emit durable undo_repair_required records for structural undo failures, and restore the stable response contract fields retro_voiceprints_saved, operation_state, and corrections_appended.
Map recoverable, repair, conflict, not-found, in-progress, and undoing outcomes to non-2xx route envelopes. Keep store read paths write-free, extend dismissal filtering to the speakers status cluster surface, fail closed on absent or unreadable edge state while scanning edge sources, and resurface keep-separate curation rows once the assertion is tombstoned or the watermark is exceeded.
Remove the dead resolved-cluster writer, correct the lock-order docs, and reject duplicate reviewed near-match ids explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(speakers): expose identify operations and suppression
Add HTTP and sol call surfaces for identify replay/undo, operation list/show, cluster dismissals, and keep-separate summaries. Recoverable, repair_required, conflict, and not_found results now map to non-2xx standard error envelopes with operation state in extra, while genuine identify and undo success keep 2xx responses; the old status:"partial" response shape is not retained.
Wire the shared keep-separate predicate through name-variant record-time storage, curation read-time loading, and suggest re-detection so suppressed pairs stop nagging until detection_count exceeds the watermark. Add the dismissal read-side filter, generated OpenAPI and command references, and route/CLI/contract coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
test(convey): collect the dark convey tests
solstone/convey/tests/ was never collected by CI: pyproject's testpaths
is ["tests", "solstone/apps"] and the Makefile passes the same two
paths, so ten modules created in May and maintained as recently as
2026-07-18 had never once run. Nothing signalled deliberate exclusion —
.coveragerc already lists solstone/convey as a coverage source, which is
the opposite. Orphaned by omission.
Moved into tests/ rather than adding convey to testpaths: solstone/apps
was added there by 226084fbf "add BYOT (bring your own tests) support
for apps", an app-scoped mechanism, and convey is the dashboard host
that serves the apps, not an app. tests/ already holds 19
test_convey_*.py modules and 106 modules importing solstone.convey.
Kept 8 modules whose coverage exists nowhere else (generate_request_id,
the X-Solstone-Request-Id header, SEND_FILE_MAX_AGE_DEFAULT, the
sol-status mark assets and deriveStatusMark, the fail-loud
contract-startup paths, init brand-canon guards, init mark-lock
data-integrity guards) plus the SSE module.
Dropped test_init_html.py: two of its four tests were subsumed by its
own sibling test_init_template.py::test_init_provider_section_is_basics_only;
its two unique assertions are folded in there instead.
Dropped test_callosum_sse.py's slow-subscriber test: duplicated verbatim
by solstone/apps/observer/tests/test_callosum_sse.py, which CI already
collects, and it carried the module's only wall-clock assertion. Renamed
the module to test_convey_callosum_sse.py so the basename is unique.
Fixed four __file__-relative path constants that assumed the old depth,
and folded conftest's two fixtures into tests/conftest.py — which also
gains them its autouse hygiene, including
SOLSTONE_DISABLE_CONVEY_SIDE_RUNTIMES=1 that convey's own conftest
lacked.
40 tests, 4.1s. Full tests/ run: 12139 passed, 6 failed — the 6 are
pre-existing release-host assertion drift on main, unrelated and
reproduced before this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XsyxhzTzKkzZ4J7i4EBaD
Route mutation lanes through entity resolution boundary
Move the audited journal-writing name-resolution lanes onto record_entity_resolution so tier 5-8 matches persist ambiguity before any domain write and then reuse each lane's existing unresolved representation.
This prevents low-confidence names from creating duplicates, attaching speaker labels or voiceprints, or firing irreversible identity merges. In particular merge.py now stages ambiguous source entities, speakers.merge_names refuses before merge_entity(commit=True), and discovery exits before entity creation, voiceprint saves, labels, resolved-cluster writes, and candidate retroactive confirmation.
The import bridge checks resolved choices before the identical-content shortcut, so a staged low-confidence entity can be resolved and then applied on re-ingest without leaving received, id_map, or staging state incoherent.
feat(schemas): name-first relational layer for story + entity_observer
Add a bounded relational layer to two generation schemas. Models emit entity
names; Python resolves them to entity IDs after generation. An unresolved name
never becomes a fabricated ID.
story.schema.json gains a required top-level `relations` array
({from, to, kind, note, quote}) and a required nullable `counterparty` on
decisions. story.py resolves relation endpoints and decision counterparties
through the existing find_matching_entity(fuzzy_threshold=90) path, leaving
*_entity_id as None on a miss while the name and note/quote evidence survive
to disk. Relations with a kind outside the closed 8-value enum, or kind
"other" with no note, are skipped with a warning -- mirroring the existing
ALLOWED_RESOLUTIONS precedent.
entity_observer operations gain an optional nullable `relation` component with
a model-emitted target_name. An unresolvable target drops the whole op, logs a
warning, and increments a `relation_unresolved` counter surfaced in
<day>_observer_outcome.json. A relation-bearing op lands complete or not at
all.
Relation persistence stays with the L2 write owners: activity records via
activities.py::merge_story_fields, observations via
entities/observations.py::_new_observation. The app hook writes neither.
The relation-kind vocabulary lives in exactly one Python frozenset
(story.ALLOWED_RELATION_KINDS) plus the two JSON enums, with parity tests.
Both schemas are now fully bounded (maxItems on every array, maxLength on
every free-text string, including nullable ones), so their
check_schema_bounds.py allowlist entries are deleted -- the guard fails CI on
stale entries. max_output_tokens=12288 on the three story talents is derived
from the bounded schema's 31192-char theoretical maximum (~8912 tokens, within
0.8x the budget); the resulting LLM request timeout is 286s.
test_schema_prep.py's byte-identity snapshot asserted that no shipped schema
carries provider-stripped keywords -- a claim the schema-bounds ratchet is
designed to falsify. Replace it with the durable invariant: prep always yields
a provider-supported subset, is a no-op exactly when the schema has no
unsupported keywords, and demonstrably rewrites the schema when it does. Add
explicit provider-behavior tests for the newly bounded schemas (local keeps
bounds; openai/google lose maxLength; anthropic loses maxLength and maxItems),
guarded against passing vacuously.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(sol): remove Python sol skills implementation
Relocate _discover_project_sources and _ROUTER_SKILL_NAMES into solstone/think/doctor.py rather than sharing them, because doctor is still Python and must not shell out to the native binary. They are knowingly duplicated with core/crates/solstone-core-sol/src/skills.rs, and that duplication ends when doctor itself is ported.
Exactly one assertion is knowingly dropped for a reason other than the build verb removal: the duplicate-router-name ValueError branch from the original skills_cli.discover_project_sources. It is unreachable now that the required payload names are a declared constant of two distinct literals.
The build verb is removed from the CLI surface as a deliberate, operator-approved change. Its sys.path.insert plus from scripts import build_skill_references scaffolding is deleted outright, not ported. make skills invokes scripts/build_skill_references.py directly as of the preceding lode.
Delete scripts/capture_skills_parity_oracle.py in this same commit. The harness drives solstone.think.skills_cli directly: it subprocesses python -m solstone.think.skills_cli and monkeypatches skills_cli.get_project_root and skills_cli.resources.files. Once that module is gone, the script cannot run at all; it would be dead code, not merely stale. Its provenance role is fully preserved: the harness and the 30 vectors it produced are committed together at 5240cfaaf, strictly before this deletion, so git log over core/fixtures/native-sol/skills-parity-v1/ still proves the vectors were captured from the live Python oracle. The frozen vectors themselves remain in the tree and continue to gate the native implementation.
Leave solstone/think/setup.py deliberately untouched. Its skills_user_command() and skills_journal_command() build python -m solstone.think.sol_cli skills install ..., but skills is in JOURNAL_ACCESS_ONLY_COMMANDS, so sol_cli rejects it with exit 2 and journal setup reports both skills_user and skills_journal as failures. That defect predates this lode and is left for a follow-up; tests/test_setup.py and tests/test_setup_jsonl.py pin the current argv shape and pass unmodified.
Co-Authored-By: OpenAI Codex <codex@openai.com>
feat(catchup): durable segment-repair degradation lane for daily repair fairness
The daily segment_think repair pre-phase already bounds itself and emits a
machine-readable timeout in its phase.complete sidecar, but nothing downstream
reasoned about that degradation: a degraded/timed-out pre-phase let daily
synthesis advance daily.updated, so day_is_complete() returned True and every
caught-up/health surface reported the day as fully caught up — and a poison day
could consume catchup drain slots forever without backing off.
Add a durable, (day, raw-input-fingerprint)-keyed segment-repair lane to the
existing source-layer catchup-state model, reusing its backoff/threshold/
retention discipline:
- catchup_state.py: new KIND_SEGMENT_REPAIR lane with record_segment_repair_
attempt/outcome writers and a fail-closed read_segment_repair_summary reader;
success clears the lane, repeated degradation advances backoff -> quarantine
at STUCK_THRESHOLD, a fingerprint change reopens a fresh attempt. All catchup-
state read-modify-writes are now guarded by hold_lock for cross-process safety
(thinking subprocess + supervisor both write the file).
- thinking.py: records the lane at the daily pre-phase boundary (degraded on
failure carrying wall_clock_exceeded/timeout_seconds/bounded, cleared on
success) for both supervisor-spawned and manual runs.
- supervisor.py: run_catchup_drain skips active/backed-off/quarantined repair
days (unless the fingerprint changed) while later eligible days still fill
slots.
- pipeline_health.py / journal_stats.py: fold the lane into read_backlog_view
with distinct reason codes (segment_repair_degraded/stuck/unknown) mapped onto
PENDING/STUCK/UNKNOWN, fix the day_is_complete COMPLETE short-circuit to
consult repair state first, and set BacklogView.degraded so doctor caught-up,
stats totals, convey verdict, and the health UI all go honest. Unwritable/
unreadable/malformed state surfaces as UNKNOWN, never caught up. Additive
fields only — no stats SCHEMA_VERSION bump.
- check_journal_io_access.py: register catchup_state.py as the owner of its
ops/runtime state file (it now uses hold_lock).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(supervisor): move provider restarts into reconciler
Provider processes are no longer restarted by the generic service-restart path. Previously a llama, MLX, or Parakeet exit could relaunch with the original argv and no env, silently dropping GGML_VK_VISIBLE_DEVICES and LD_LIBRARY_PATH while skipping re-probe, re-tier, and warmup. Their exits now report to the provider reconciler, which clears the captured plan, re-observes truth, and decides the next action. Non-provider services such as cortex, sense, spl, and convey keep the existing restart policy unchanged.
Add a fenced provider probe slot to drive ready and ready-proof-unavailable transitions from the provider module probe seams. Rehome the cortex wedge counter action from a synchronous service restart on the Callosum callback thread to a durable runtime-health retry request that wakes reconciliation.
Make recovery metadata per-provider so a Parakeet exit no longer marks local down. Local catchup nudges now fire once per local recovery generation, and the wedge restart-machinery and local_server_down tests were rewritten to assert the reconciler-owned contract.
feat(schemas): name-first relational layer for story + entity_observer
Add a bounded relational layer to two generation schemas. Models emit entity
names; Python resolves them to entity IDs after generation. An unresolved name
never becomes a fabricated ID.
story.schema.json gains a required top-level `relations` array
({from, to, kind, note, quote}) and a required nullable `counterparty` on
decisions. story.py resolves relation endpoints and decision counterparties
through the existing find_matching_entity(fuzzy_threshold=90) path, leaving
*_entity_id as None on a miss while the name and note/quote evidence survive
to disk. Relations with a kind outside the closed 8-value enum, or kind
"other" with no note, are skipped with a warning -- mirroring the existing
ALLOWED_RESOLUTIONS precedent.
entity_observer operations gain an optional nullable `relation` component with
a model-emitted target_name. An unresolvable target drops the whole op, logs a
warning, and increments a `relation_unresolved` counter surfaced in
<day>_observer_outcome.json. A relation-bearing op lands complete or not at
all.
Relation persistence stays with the L2 write owners: activity records via
activities.py::merge_story_fields, observations via
entities/observations.py::_new_observation. The app hook writes neither.
The relation-kind vocabulary lives in exactly one Python frozenset
(story.ALLOWED_RELATION_KINDS) plus the two JSON enums, with parity tests.
Both schemas are now fully bounded (maxItems on every array, maxLength on
every free-text string, including nullable ones), so their
check_schema_bounds.py allowlist entries are deleted -- the guard fails CI on
stale entries. max_output_tokens=12288 on the three story talents is derived
from the bounded schema's 31192-char theoretical maximum (~8912 tokens, within
0.8x the budget); the resulting LLM request timeout is 286s.
test_schema_prep.py's byte-identity snapshot asserted that no shipped schema
carries provider-stripped keywords -- a claim the schema-bounds ratchet is
designed to falsify. Replace it with the durable invariant: prep always yields
a provider-supported subset, is a no-op exactly when the schema has no
unsupported keywords, and demonstrably rewrites the schema when it does. Add
explicit provider-behavior tests for the newly bounded schemas (local keeps
bounds; openai/google lose maxLength; anthropic loses maxLength and maxItems),
guarded against passing vacuously.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(schemas): name-first relational layer for story + entity_observer
Add a bounded relational layer to two generation schemas. Models emit entity
names; Python resolves them to entity IDs after generation. An unresolved name
never becomes a fabricated ID.
story.schema.json gains a required top-level `relations` array
({from, to, kind, note, quote}) and a required nullable `counterparty` on
decisions. story.py resolves relation endpoints and decision counterparties
through the existing find_matching_entity(fuzzy_threshold=90) path, leaving
*_entity_id as None on a miss while the name and note/quote evidence survive
to disk. Relations with a kind outside the closed 8-value enum, or kind
"other" with no note, are skipped with a warning -- mirroring the existing
ALLOWED_RESOLUTIONS precedent.
entity_observer operations gain an optional nullable `relation` component with
a model-emitted target_name. An unresolvable target drops the whole op, logs a
warning, and increments a `relation_unresolved` counter surfaced in
<day>_observer_outcome.json. A relation-bearing op lands complete or not at
all.
Relation persistence stays with the L2 write owners: activity records via
activities.py::merge_story_fields, observations via
entities/observations.py::_new_observation. The app hook writes neither.
The relation-kind vocabulary lives in exactly one Python frozenset
(story.ALLOWED_RELATION_KINDS) plus the two JSON enums, with parity tests.
Both schemas are now fully bounded (maxItems on every array, maxLength on
every free-text string, including nullable ones), so their
check_schema_bounds.py allowlist entries are deleted -- the guard fails CI on
stale entries. max_output_tokens=12288 on the three story talents is derived
from the bounded schema's 31192-char theoretical maximum (~8912 tokens, within
0.8x the budget); the resulting LLM request timeout is 286s.
test_schema_prep.py's byte-identity snapshot asserted that no shipped schema
carries provider-stripped keywords -- a claim the schema-bounds ratchet is
designed to falsify. Replace it with the durable invariant: prep always yields
a provider-supported subset, is a no-op exactly when the schema has no
unsupported keywords, and demonstrably rewrites the schema when it does. Add
explicit provider-behavior tests for the newly bounded schemas (local keeps
bounds; openai/google lose maxLength; anthropic loses maxLength and maxItems),
guarded against passing vacuously.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(convey): remove web-UI password, login, and session auth
port 5015 is loopback-only, and mtls on 7657 is the sole network transport as of 0.6.0, so the web password gated nothing. remove the login form, flask session-cookie login, http basic auth, the trust_localhost localhost bypass, the journal password cli, and the convey.secret/app.secret_key flask-session machinery.
refactor the require_login gate to require_access while keeping private link revocation auth and the first-run /init onboarding redirect. there is no migration: orphan convey.password_hash, convey.secret, and trust_localhost keys in existing journal.json files are ignored, and _NEVER_TRANSFER_PATHS still scrubs password_hash and secret on export.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(talent): schema-bounded JSON for morning briefing
morning_briefing now emits schema-bounded JSON via the new morning_briefing.schema.json file. The schema is fully bounded and does not require any check_schema_bounds.py ALLOWLIST entry. A day-level formatter registry entry renders the JSON output back to markdown for the search index and the identity CLI, preserving the five ## headings and the morning_briefing indexer agent string that entities/context.py's NOISE_AGENTS depends on.
morning_briefing_path() previously hard-coded output_format="md", the single hard-coded lie in the path authority for this talent. It now derives the extension from the talent's declared output format through a cached _briefing_output_format() helper, so briefing_cmd's day_dirs() loop does not re-read the talent config once per day.
The consumer migration is a clean break, not a compatibility shim. _load_briefing_md and _BRIEFING_SECTIONS are deleted, and Home, voice, and the identity CLI all read through the new shared solstone/think/briefing.py loader and renderers. This incidentally removes the solstone.think.voice.tools -> solstone.apps.home.routes import, a think-to-apps dependency inversion logged as a follow-up during recon.
A post_process hook was considered and declined. The empirical finding was that _run_post_hooks runs before _write_output on both talents.py:1375 and talents.py:1673, while _schema_validation_clean at talents.py:1087 rejects on the provider's gen_result["schema_validation"] before re-validating the post-hooked string, so a post-hook cannot repair a required key the model omitted. Because every schema root is additionalProperties: false, a post-hook cannot inject an undeclared key either. OpenAI strict mode forbids optional properties, so "declared but not required" is unavailable; the model must emit the key regardless, leaving the hook's only residual value as overwriting values the model was already forced to produce. talents.py:1822 also pops template_vars off modifications and never stores it on config, so a post-hook cannot even see the pre-hook packet without inventing a new pre-hook return-key convention that exists nowhere else in the repo. Instead, the pre-hook collapses generated, model, source_counts, source_gaps, and coverage_preamble into one $briefing_metadata template var built with json.dumps(..., indent=2), retiring the two-space YAML fragment _render_source_counts used to emit and removing the JSON-escaping hazard for the preamble prose. date is dropped entirely because the path encodes the day.
Only lowercase $briefing_metadata is safe in the prompt. _apply_template_vars at talents.py:852-861 registers a .capitalize() alias that would lowercase the rest of the JSON string, so the prompt uses the lowercase token only and a test pins that constraint.
tests/eval_schemas.py now runs schema_path cases through hydrate_runtime_enums so the eval validates the runtime schema the provider actually receives. Without this, the reading[].facet __RUNTIME_FACETS__ sentinel would force the eval model to emit that literal string and the new golden case could never pass. reading[].facet carries both the sentinel enum and a maxLength because hydrate_runtime_enums drops the enum key on a zero-facet journal and the string would otherwise ship to the provider unbounded.
_strip_outer_markdown_fence runs only for output_format == "md" at talents.py:1658-1668, so JSON output is not fence-stripped. The prompt's explicit no-fence instruction is the only mitigation, matching every other output: json talent. This is not a new condition, but it is recorded here so the next reader does not assume the runner strips JSON fences.
max_output_tokens: 8192 now matches documents; the talent previously inherited the 8192 * 6 default silently. degradation_check: true is retained for the same reason 9654c0d0 retained it on documents: four other json-output talents set it, so briefing is not special. A near-empty briefing sits under the MIN_OUTPUT_TOKENS = 300 floor and could be falsely flagged degraded on a sparse day; this remains a follow-up risk.
The two API baselines flip legitimately. tests/baselines/api/sol/talents-day.json moves output_format from md to json. tests/baselines/api/stats/stats.json moves output from md to json and additionally gains schema and max_output_tokens because that API exposes raw prompt frontmatter and every other talent with those fields already shows them there. Nothing else in either file moved.
Behavior is otherwise held constant across the web contract. briefing_sections stays a dict of markdown strings and briefing_needs_deduped stays an array of strings, so home.js and workspace.html are untouched. Needs items are now objects carrying text plus a sol:// source_id, which needs_dedup_key resolves by identity; inline [label](sol://...) links stay inside text so the existing parse_sol_sources fallback keeps working. _briefing_summary now counts meetings from structured your_day items with a non-empty time rather than regexing - **HH:MM**, and produces a byte-identical string for equivalent content.
One deliberate behavior change remains: reading links now resolve to facet=<slug>, for example facet=work, where the model previously wrote **Work** and produced facet=Work. No test pinned the old casing, and the slug is what /app/search expects.
The local Qwen provider is not installed on this machine, so the eval was not executed and no pass is claimed. The command below was run with this output.
```
$ make eval-schemas; echo "exit=$?"
.venv/bin/python tests/eval_schemas.py
Local schema eval requires the bundled local provider. Run `journal install-provider local`, then start it with `journal start` (or `journal service start` for an installed service).
make: *** [Makefile:234: eval-schemas] Error 2
exit=2
```
Follow-ups deliberately not done: pulse.schema.json still has 5 unbounded nodes and steward.schema.json still has 2, both allowlisted under their "morning_briefing follow-on lode" reason, following the convention that the string names the lode a following lode will bound. solstone/apps/speakers/status.py's meetings_files counter still has the same wrong-path bug 9654c0d0 fixed for screen_files. docs/design/yesterdays-processing-card.md and docs/design/voice-server.md still describe frontmatter-based briefing reads in narrative prose beyond the one-line path references corrected here. No confidence field was added to briefing items; evidence strength stays prose hedging because a field would change rendering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(health): alarm on near-empty cogitate/generate talent runs
When a degradation-checked talent finishes "successfully" but the model
produced near-zero output (output_tokens below MIN_OUTPUT_TOKENS=300),
annotate the finish event with a `degraded: {reason: near_empty,
output_tokens}` marker. Cortex carries the marker into the
journal/talents/<day>.jsonl day-index row; the operational health surface
counts talent_degraded_outputs_24h over the 24h window and emits a per-run
warn note (talent + provider/model + day + token count, capped at 10) that
flows to the steward synthesis context. Closes the gap where a weak-fallback
provider outage silently degraded synthesis with no visible signal.
Detection is opt-in per talent via a `degradation_check` frontmatter flag,
enabled on the reliably-substantial synthesis talents (morning_briefing,
weekly_reflection, documents, conversation, event, work). Most talents
legitimately finish small (tool-acting cogitates, empty schedules/
participation, quiet-segment sense), so a global token floor would
false-positive; opt-in keeps the alarm credible and future-safe. Token-only
classification means diverting story-hook talents (high model tokens, empty
post-hook result) never false-positive, and skip/cached-reload/error/quota/
max-turns runs never carry the marker. The day-index field is additive
(absence reads as not-degraded); no provider key or fallback credential is
wired this arc.
Updates the stats API baseline to reflect the new frontmatter flag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
fix(cortex): let the orchestrator wait out a lost claim broadcast
Callosum is a best-effort broadcast bus. When a burst of logs.line traffic
caused the server to evict a slow consumer, cortex talent requests broadcast in
that window were simply lost. cortex_request's claim wait was a fixed 3 sends
about 1.0s apart, so a lost request exhausted the schedule in ~3s and raised
CortexNotClaimed. For the think orchestrator that became talent.fail
state=request_lost and the unit failed, to be re-walked hours later: 83 such
failures over 24h on a single-GPU local-model install, 67 of them on sense.
A delivered request claims in milliseconds, so the schedule exists to survive a
lost broadcast, not a slow one. Make it a per-call choice. cortex_request gains
a keyword-only claim_windows, where len(windows) is the total send count
including the initial broadcast and element i is the poll window after send i.
The default (1.0, 1.0, 1.0) reproduces today's ~3s fast-fail exactly, so
interactive callers -- convey chat, tools/sol, engage -- keep their latency and
take no diff. _dispatch_cortex_request, which fronts all seven thinking.py
spawn sites, opts into PATIENT_CLAIM_WINDOWS (1, 2, 4, 8, 15; ~30s budget).
Replace _CLAIM_WINDOW_S and _CLAIM_MAX_BROADCASTS with the two schedules; the
window walk makes the send count structurally equal to len(windows) rather than
implied by a max-broadcasts counter. Resolve the default at call time so tests
can monkeypatch it.
Widen the 49 cortex_request test mocks to accept the new keyword. The mocks
intercept think.cortex_request, so they must tolerate what the wrapper passes;
the alternative -- swallowing unknown kwargs in production -- would be a compat
shim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(entities): single-source entity I/O on journal_io primitives
Migrate the entity/observation domain's write mechanics onto the shared
solstone.think.journal_io primitives.
- Five writers (save_journal_entity, save_facet_relationship,
_save_entities_detected, save_observations, review-candidate
_save_jsonl_rows) move from core.atomic_write to atomic_replace, preserving
byte-exact serialization (ensure_ascii=False, indent, trailing newline) and
gaining fsync durability.
- Three locked RMWs (add_observation, _locked_modify_detected,
locked_modify_candidates) route through hold_lock, each keeping its existing
retry policy (observations + detected: 3x OSError retry; candidates: no
retry). LockTimeout (a RuntimeError) propagates past the OSError loops by
design. The bespoke review-candidates lock sidecar is replaced by hold_lock's
convention; review_candidates_lock_path() is removed.
- Surface the new LockTimeout failure mode as ENTITY_BUSY (503, owner-voice)
across the CLI entities verbs, the curation accept/dismiss entity routes, and
the entity-observer talent + seeding (log-and-skip).
- Register relationships.py as an Entities write-owner (AGENTS.md L2 +
check_journal_io_access.py OWNER_FILES); drop the dead atomic_write re-export
from entities/__init__.py.
- Tests: repoint the mechanic-patching retry test to atomic_replace, add an
observations retry test and a candidate no-retry test, and add a cross-process
serialization test covering all three RMWs.
core.atomic_write is retained for its six out-of-domain callers (deletion
deferred to a follow-up lode). merge.py _apply_segment_plan's non-atomic
temp-write is flagged for R3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(providers): size describe fan-out by local capacity
Move describe and segment fan-out policy into one provider module and size
describe's per-process concurrency from the product, not from a second
hard-coded default. The target bound is:
effective_procs x per_proc <= 2 x slots
The 2x factor is deliberate. One request can be serving while one is
pipelined across client-side frame prep, which keeps the local model slot
busy without leaving describe's own -j default as an unbounded multiplier.
Direct describe resolves as one effective proc; sense passes an explicit
per-proc -j derived from its actual handler width.
The bound is intentionally scoped. It holds whenever effective_procs <= 2N.
Beyond that, an operator's explicit describe.max_concurrent wins and the
product floors at effective_procs. The known residual is the 3 cells at
describe.max_concurrent=4 x slots=1; those are logged at info once per key.
acquire_local_slot still bounds real in-flight provider requests regardless.
Concurrent think/sense processes on different partitions can still overlap;
that per-invocation residual remains unbounded and out of scope here.
This corrects d0eb97ae's stale close-out claim that worst-case concurrent
model-bound calls dropped from ~6-16 to 2x slots. That was false on the
describe path: it reasoned about the segment-worker multiplier but missed
describe's own multiplier, the -j 10 argparse default that sense.py never
overrode. Real worst case was slots x 10. This commit makes the bounded
claim true for a single invocation. d0eb97ae also said BYO keeps the CPU
formula and never probes; later BYO slot governance made that no longer true.
For BYO generate errors, split capacity from unreachable by exact exception
name over the cause chain, with capacity winning mixed chains. This is a
deliberate semantic broadening, not a parity restoration. Previously
local_capacity_exhausted was raised only on the bundled path, only via
LocalCapacityExhausted from an HTTPStatusError whose body matched
_CONTEXT_WINDOW_PATTERNS: the server answered that it was full. BYO timeouts
have no HTTP response, but now classify as local_capacity_exhausted so the
existing retry path can run instead of treating saturation as endpoint down.
Named blast radius: talents.py:1485 keys capacity_retry on this exact reason
code, so BYO timeouts now get a retry in talent lanes instead of being fatal.
That is intended. classify_provider_error already makes this split on the
non-BYO path; test_provider_error_classification.py:205 pins
httpx.ReadTimeout -> chat_timeout while httpx.ConnectError ->
network_unreachable. BYO generate was the outlier.
WriteTimeout is an addition, not a re-sort. It was not in
_BYO_NETWORK_EXC_NAMES and previously fell through to
local_endpoint_contract_failed.
Darwin bundled local remains slots=1 from the default/no-props path but now
resolves per-proc describe concurrency to 2, not 1. That supersedes
d0eb97ae's rationale that 1 was correct because mlx-vlm serves one request
at a time; the 2x pipelining rationale is the governing one.
fix(transcribe): route STT by confidential usability
- Bug: a stranded journal with services.confidential present and
providers.local emptied of its credential made STT callers treat the
confidential lane as active from bare block presence. They selected
the confidential backend, then deferred forever in
confidential.py::_headers; transcription silently stopped while the
journal looked healthy.
- Add spp.is_confidential_channel_usable(config):
is_confidential_enabled(config) and not
resolve_local_endpoint_from_config(config).is_bundled.
It is config-pure: no network and no attestation probe.
- Do not adopt is_confidential_enabled alone. It returns True for a
credential-only config with no endpoint_url or served_model_id; that
shape fails the forwarder probe with endpoint_invalid and would strand
STT identically.
- Keep the asymmetry explicit: routing follows channel usability. The
egress refusal at observe/transcribe/__init__.py:204 stays on bare
block presence because it answers whether raw audio may go remote, and
it remains fail-closed and broad.
- Changed observe/transcribe/main.py::resolve_default_backend;
think/supervisor.py::linux_stt_uses_parakeet_cpp; and
think/supervisor.py::_observe_parakeet_provider_truth.
- In _parakeet_stt_admission_latch, all four uses now carry usability:
payload/fingerprint key value, RAM-read short-circuit, selector call,
and ram_blocked.
- Changed apps/settings/routes.py to pass usability into
apps/settings/transcribe_resource.py, the direct selector caller the
original scope map missed.
- Add three-way warning copy in main.py so explicit confidential fallback
distinguishes disabled confidential audio, no confidential lane, and an
incomplete channel that names missing fields.
- Widen fixtures without weakening assertions. test_no_implicit_cloud.py
uses _add_local_endpoint; both assertions are verbatim.
- settings test_transcribe_resource_payload seeds endpoint_url,
served_model_id, and credential; assertions are unchanged.
- test_supervisor_parakeet.py and test_transcribe_cli.py replace
monkeypatch-provenance-alone usable-channel setups with complete usable
provider config where that was the intent.
- Deliberate AC5 behavior change: stranded low-RAM now exits via
SystemExit(1) from resolve_default_backend instead of
EXIT_PROVIDER_BLOCKED (69) from deferral.
- Data-safety finding: input audio remains on disk, no JSONL is written,
_process_one is never reached, resolve_segment_gate reports
incomplete, and sense's non-69 path does not delete, move,
quarantine, or mark-failed the audio.
- No config migration: confidential_lane_active is part of
_parakeet_stt_admission_input, so old latch records hash to a
different input_sha256 and are recomputed on the first tick.
- Red-first evidence: AC1 and AC5 failed with "DID NOT RAISE
SystemExit"; AC2 failed selecting confidential instead of parakeet;
AC7 failed with not-desired instead of host-blocked and
artifact-not-ready. AC6 was green on main by construction.
- Reported, not fixed: linux_stt_uses_parakeet_cpp is dead in
production; apps/settings/routes.py:209 backend coercion makes the
settings needs_setup/notice path unreachable; routes.py:590 uses
bool(...) instead of confidential_audio_enabled's true-when-unset
default; the confidential_lane_active selector keyword and latch
payload key remain a deferred rename.
- Validated before commit: focused STT/settings/retention suites passed,
then make ci passed with 14738 passed and 16 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(transcribe): route STT by confidential usability
- Bug: a stranded journal with services.confidential present and
providers.local emptied of its credential made STT callers treat the
confidential lane as active from bare block presence. They selected
the confidential backend, then deferred forever in
confidential.py::_headers; transcription silently stopped while the
journal looked healthy.
- Add spp.is_confidential_channel_usable(config):
is_confidential_enabled(config) and not
resolve_local_endpoint_from_config(config).is_bundled.
It is config-pure: no network and no attestation probe.
- Do not adopt is_confidential_enabled alone. It returns True for a
credential-only config with no endpoint_url or served_model_id; that
shape fails the forwarder probe with endpoint_invalid and would strand
STT identically.
- Keep the asymmetry explicit: routing follows channel usability. The
egress refusal at observe/transcribe/__init__.py:204 stays on bare
block presence because it answers whether raw audio may go remote, and
it remains fail-closed and broad.
- Changed observe/transcribe/main.py::resolve_default_backend;
think/supervisor.py::linux_stt_uses_parakeet_cpp; and
think/supervisor.py::_observe_parakeet_provider_truth.
- In _parakeet_stt_admission_latch, all four uses now carry usability:
payload/fingerprint key value, RAM-read short-circuit, selector call,
and ram_blocked.
- Changed apps/settings/routes.py to pass usability into
apps/settings/transcribe_resource.py, the direct selector caller the
original scope map missed.
- Add three-way warning copy in main.py so explicit confidential fallback
distinguishes disabled confidential audio, no confidential lane, and an
incomplete channel that names missing fields.
- Widen fixtures without weakening assertions. test_no_implicit_cloud.py
uses _add_local_endpoint; both assertions are verbatim.
- settings test_transcribe_resource_payload seeds endpoint_url,
served_model_id, and credential; assertions are unchanged.
- test_supervisor_parakeet.py and test_transcribe_cli.py replace
monkeypatch-provenance-alone usable-channel setups with complete usable
provider config where that was the intent.
- Deliberate AC5 behavior change: stranded low-RAM now exits via
SystemExit(1) from resolve_default_backend instead of
EXIT_PROVIDER_BLOCKED (69) from deferral.
- Data-safety finding: input audio remains on disk, no JSONL is written,
_process_one is never reached, resolve_segment_gate reports
incomplete, and sense's non-69 path does not delete, move,
quarantine, or mark-failed the audio.
- No config migration: confidential_lane_active is part of
_parakeet_stt_admission_input, so old latch records hash to a
different input_sha256 and are recomputed on the first tick.
- Red-first evidence: AC1 and AC5 failed with "DID NOT RAISE
SystemExit"; AC2 failed selecting confidential instead of parakeet;
AC7 failed with not-desired instead of host-blocked and
artifact-not-ready. AC6 was green on main by construction.
- Reported, not fixed: linux_stt_uses_parakeet_cpp is dead in
production; apps/settings/routes.py:209 backend coercion makes the
settings needs_setup/notice path unreachable; routes.py:590 uses
bool(...) instead of confidential_audio_enabled's true-when-unset
default; the confidential_lane_active selector keyword and latch
payload key remain a deferred rename.
- Validated before commit: focused STT/settings/retention suites passed,
then make ci passed with 14738 passed and 16 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(packaging): scaffold macOS Apple Silicon platform wheel
Bump solstone to 0.2.0, narrow package-data to four explicit parakeet helper entries in place of the two greedy globs, add _bin/parakeet-helper precedence to both _helper_path and _resolve_helper_path, and rewrite PACKAGED_COREML_HINT for the post-option-A packaged-install behavior. Add Darwin/arm64-gated make wheel-macos and make wheel-macos-clean targets, plus scripts/sign-and-notarize-helper.sh for Developer ID signing and notarytool submission.
This is stage-1-of-2 of the founder-approved plan at vpe/workspace/plan-solstone-macos-platform-wheels.md, req_aubczank, 2026-05-08. Stage 2 is VPE-direct on pro5e: building and signing the helper on macOS, producing the multi-wheel release (sdist + py3-none-any + py3-none-macosx_14_0_arm64), and publishing to PyPI.
Reframe INSTALL.md and CONTRIBUTING.md so packaged installs on macOS 14+ Apple Silicon now ship the helper pre-built, while source-checkout installs still build it locally for development.
Add in-scope precedence coverage for _helper_path and _resolve_helper_path in test_install_models.py and test_transcribe_parakeet_coreml.py, and update test_install_verify.py and test_journal_export.py for the 0.2.0 version string.
Signed-off-by: Jer Miller <jeremie.miller@gmail.com>
feat: place Parakeet STT on CPU when GPU cannot co-locate
On Linux hosts where the bundled brain is resident on a single discrete GPU that cannot also hold a worst-case ordinary-segment Parakeet run, transcribe.parakeet-cpp.device: auto now resolves to CPU at parakeet-server launch.
That decision uses measured floor-tier brain residency 4541 MiB + Parakeet worst case 5022 MiB + 1024 MiB margin = 10587 MiB. As a result, 6/8/10 GiB cards place STT on CPU, while 11/12 GiB and up keep it on the GPU.
Every other state fails toward today's behavior: unknown VRAM, probe failure, unmeasured capable-tier residency, 2+ discrete GPUs, integrated or unified memory, or an inactive bundled-brain lane keep auto resolving exactly as before.
The placement is announced through the supervisor reason line, GPU-check detail in sol check and the local provider fit report, and journal/health/parakeet-cpp.placement so transcribed events and transcript headers report the real device placement, cpu/gpu, rather than literal auto. The checks remain ok because this is a hardware-fit fact, not a failure.
No config keys were added; transcribe.parakeet-cpp.device still validates as auto | cpu only.
fix(link): durable sol link serve tunnel (client keepalive + reconnect + fail-fast)
An idle paired-link tunnel went dead within ~90s and never recovered: requests
through the loopback proxy hung forever instead of failing fast. The client mux
ignored stream-0 control frames entirely (it RESET them), never drove keepalive,
and cached the tunnel session unconditionally with no liveness gate or
establishment timeout. Server-side PING/PONG and the framing primitives were
already correct and complete; this finishes the client side only — no server,
wire-protocol, or serve_cli behavior change.
- _DialerMultiplexer now dispatches stream-0 control frames: replies PONG to
PING, records inbound liveness, and tears the session down (self.close) on a
malformed control frame instead of emitting an illegal RESET on stream 0.
- TunnelSession drives a keepalive task (distinct-nonce PINGs every interval),
tracks last-inbound-activity on the loop clock, declares the session dead and
closes the transport after keepalive_timeout of silence, exposes is_alive, and
always reaps the keepalive task on close and on reader exit.
- TunnelClient._get_session_async gates the cached session on is_alive and
lazily re-dials on drop under the session lock (single-flight; a failed
re-dial leaves _session is None). Request-head establishment is bounded by a
fail-fast asyncio.timeout on the non-streaming request() path and around the
stream_request head only — the streaming body is never bounded.
Keepalive/establishment params are module constants plus keyword args for test
injection only (no env/config/CLI). Red-before-fix evidence: ACs 1,2,3,5,6,7,8
were demonstrably red on the pre-fix tree (no stream-0 handling, no keepalive,
no liveness gate, no establishment timeout) and green after.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(retention): add journal log/cache retention pruning
Add a conservative, journal-wide pruner for dated operational logs and
execution traces, mirroring raw-media retention. Owner-configurable in
Settings -> Storage (enabled toggle + days, default on/30d), run daily via
the new app-owned maintenance routine `health:prune-logs`, and previewable/
runnable manually (dry-run first).
Deletes only a fixed allowlist of dated log/cache classes (chronicle
<day>/health/*.{log,jsonl}, talent run logs + day indexes, tokens, awareness,
config actions, facet logs, observer history, cogitate cache); never observed
media or durable content. Keeps the cutoff day (strict D < cutoff), is
symlink-safe (never follows targets), and is idempotent.
Every actual run writes a per-affected-day task_log.txt summary and one global
health/pruning-runs/<day>.jsonl record via a single shared audit writer, which
raw-media purge() now also emits (its eligibility/safety unchanged). Disabled
config and dry-run are hard no-ops that write no audit. Per-day and global
audit-write failures surface as partial errors in the result + CLI/API.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
refactor: collapse top-level packages into solstone namespace
This is phase 1 of the namespace refactor required before distributing the project on PyPI as solstone. The five previous top-level packages (think, apps, observe, convey, talent) collide with unrelated PyPI projects, so a pip install solstone layout would clobber those packages instead of installing a single coherent distribution.
Move the Python package tree under solstone/, rewrite imports and runtime module strings, and update packaging, Makefile targets, scripts, tests, docs, and package data declarations to match the new canonical module paths. Audit and fix Path(__file__) walks for the new layout, with get_project_root() now correctly anchored at the repository root.
Preserve callosum event taxonomy, provider routing keys, journal config keys, journal data layout, and on-disk historical content. This is a clean break: no back-compat shims, aliases, fallbacks, or re-exports.
Phase 2 will handle the PyPI publish work; phase 3 will handle the INSTALL.md distribution rewrite.
Co-Authored-By: Codex <codex@openai.com>
feat(speakers): add identify ledger owner foundations
Add strict append-only JSONL owners for the identify operation ledger, cluster dismissals, and keep-separate assertions. The stores fold events instead of rewriting logical rows, which locks in the no-migration durable contract for these journal files.
Expose deterministic operation_id generation, request-fingerprint hashing, strict event validation, and folded operation state for Wave B replay. Add owner primitives for exact voiceprint-key removal, compare-before-restore labels, retroactive-confirm planning and restore, identify_undo correction rebuild semantics, and conservative created-entity deletion support.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(indexer): retire the Python command-write path
Every write-bearing journal indexer invocation now runs solstone-core indexer; query, count, display, and interactive search stay Python.
Delete the core.indexer / core.indexer_on_decline selector, the 69 decline-fallback, and the Python CLI write blocks rather than deprecating them; the command no longer reads either key.
Remove reset_journal_index and its export.
Write-bearing commands on hosts with no compatible solstone-core wheel, including musl Linux and Intel macOS, exit 78 before native execution, Python writes, or query.
Make --rescan-file with --rescan/--rescan-full exit 64 before any side effect, replacing the old partial-side-effect ordering.
Rename native_seam.py to native.py.
Teach the indexer differential harness the native edge-extraction-skip stderr wording, mirroring the existing native markdown-sanitize rule, and point the fixture-corpus commands at the retired-selector-free journal indexer invocation.