Commits
_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.
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.
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.
`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.
Cut from the root project version and propagated derived packaging state via
scripts/render_packaging.py and uv lock; no hand-edited derived files.
1.0.17 exists to carry 5a803f789, the setup access-surface fix. 1.0.16 shipped a
journal setup that failed two of its own steps and exited non-zero, which hangs
the macOS app first-run onboarding. The changelog bullet names the owner-visible
effect on both surfaces: a setup that reported a problem when nothing was wrong,
and a new Mac install that could wait forever at the last step.
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.
The batch-3 fix (0dbcb0d67) added a 12px horizontal component to .link-modal's padding shorthand. That component was cosmetic.
It narrowed .link-modal-box (width: 92%) from 331px to 309px at a 375px viewport, which produced 9px of internal horizontal overflow (scrollWidth 318 vs clientWidth 309) because the QR panel is effectively fixed at ~294px and was being clipped.
Zeroing only the horizontal component restores the box to 331px with scrollWidth == clientWidth, and the QR panel fits inside the box.
.link-modal-box { width: 92% } supplies the side gutter on its own; at every viewport that 4%-per-side gutter is at least as wide as the 12px removed.
The vertical components calc(var(--facet-bar-height) + 12px) and calc(var(--app-bar-height) + 80px) are unchanged; they keep #link-pair-cancel clear of the fixed chat bar.
Constrain `.link-modal` to the band between the facet bar and chat bar and let `.link-modal-box` scroll internally, so `#link-pair-cancel` is no longer painted under the fixed chat bar.
Cap the `is-glasses` QR at `min(100%, 360px)` instead of `100%`, so the modal no longer overflows the viewport.
Add `DEVICE_LABEL_PLACEHOLDER_COMPUTER` and set the `#link-device-label` placeholder from the selected presentation mode in `setPairCopy()`. The placeholder seam runs before a pair request exists because `setPresentationMode()` calls `setPairCopy()` ahead of `renderPairPresentation()` and its `!currentPair` guard.
Co-Authored-By: OpenAI Codex <codex@openai.com>
The mixed-writability hazard test previously detected root and returned before asserting anything, so under root execution, which is live in this repo, AC 12 had zero coverage while showing green.
Add a root-proof variant that places a regular file at one agent's skills-root path. mkdir through a non-directory component fails with ENOTDIR for every uid including root, so the vector runs everywhere with no chmod and no seam. It asserts the writable agent still installs, that an error row names the unwritable agent and path, and exit status 1. The row reason is deliberately not byte-pinned, because permission failures use Rust-native io::Error text under the divergence recorded in 9663b6445.
Retain the chmod-based test for the true permission-bits case, and make its root skip loud rather than silent so a root run reports the gap instead of showing green.
The negative control was verified: temporarily inverting the new test's exit assertion made it fail with exit 101 and left: 1 right: 0, confirming the test can actually fail.
Remove a redundant !is_symlink() clause from a symlink_metadata-sourced check in the tree walk, where is_dir() is already false for symlinks.
Co-Authored-By: OpenAI Codex <codex@openai.com>
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>
sol skills install, uninstall, and list now run through a direct match arm in solstone_core_sol::run, placed before the compatibility dispatcher. It is not a native authority entry: the inventory builder globs solstone/**/native/**/authority.toml, every entry lands in a partition with a frozen count, and sol-call paths must exist in the frozen oracle grammar. skills is a top-level non-call verb with no oracle path.
Remove skills from TOP_LEVEL_COMPAT_MODULES and TOP_LEVEL_COMPAT_COMMANDS, one element each. The compatibility dispatcher, the SOLSTONE_NATIVE_COMPAT_ACTIVE sentinel, and every other compat entry are untouched. A new order-insensitive, count-free element-identity assertion ties the two collections together so they cannot drift.
One Python/Rust divergence is knowingly accepted: install_user does not reproduce the os.access(target, W_OK) pre-check. access(2) has no std equivalent and adding libc to reproduce one error string is not warranted. The filesystem outcome is identical: a mode-0500 directory fails remove_dir_all immediately with no partial deletion. Only the reason string differs, and it is Rust-native. AC 12 is covered structurally by a hazard test rather than byte-pinned.
The project root is canonicalized inside the skills path only, matching Python's get_project_root() realpath so lexical relpath targets agree. resolve_project_root(), sol root output, and check-native-sol-root-contract are unchanged.
skills.rs is added to CORE_REQUIRED_SDIST_MEMBERS in scripts/check_wheel_contents.py because a new solstone-core-sol source file must ship in the core sdist.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Capture 30 parity vectors by running the live Python oracle in solstone/think/skills_cli.py through scripts/capture_skills_parity_oracle.py. These vectors are generated evidence, not hand-written fixtures.
This lands before that Python module is deleted later in this lode, so git log over core/fixtures/native-sol/skills-parity-v1/ proves provenance. A fixture set reconstructed by reading deleted code would not be evidence.
The harness deliberately has no --check mode and is not wired into make ci. Its oracle is deleted two commits later, so such a gate would be permanently red; this is a deliberate divergence from the scripts/build_journal_resolution_vectors.py precedent.
stdout and stderr are captured as separate streams. argparse's own usage/help/error message bodies and Python OSError.__str__ errno renderings are deliberately not pinned; for the exit-2 usage vectors only the exit status is asserted.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Drop has_cloud_model_request_mark from shared.py::__all__. Its only caller is is_cloud_model_not_found in the same module, and the repo has no star-imports from providers.shared, so exporting it added dead public surface without a consumer.
Keep the chain-wide model-request marker, but document why it marks every exception in the chain: OpenHands _unwrap_provider_exception classifies __cause__ or __context__, so a mark on only the caught wrapper could be discarded before classification.
Correct docs/design/openapi-client-contract.md for the RUNTIME_REASON_CODES line shift introduced by this branch.
Validation record: the full make ci gate passed on the preceding tree with 15193 passed, 17 skipped, and Rust checks included. The two failures recorded in 270a13d11's message were subsequently attributed to an intermittent full-suite isolation issue with no causal path to this diff; those tests depend on observer-bundle temp-root hashing and speakers temp-journal route state, neither of which this branch touches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Specialize model_not_found copy only in the dynamic provider readiness view, where provider and model are both available. When a model id is present, the visible summary and detail now name that selected model; when model is falsy, the existing _ENTRIES text is rendered unchanged.
The static registry stays generic because _ENTRIES projects verbatim to chat_reasons.js and command.rs::readiness_summary, and those model-less surfaces do not have model identity. Adding a {model} token there would leak an unresolved placeholder into static projections instead of improving the runtime view.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stamp model-request provenance only around the OpenHands llm.responses / llm.completion transport invocations and their async equivalents. Those helpers are reached by built-in cloud generate and the validation probe; local and BYO endpoints route through local.py instead, so they never receive this marker.
A generic 404 now becomes model_not_found only when the marker is present in the exception chain and some chain item reports status 404. Trusted provider SDK NotFound-shaped classes keep the identity path, constrained to litellm.exceptions, openai, and anthropic namespaces, so lookalike local classes remain unknown. The chain status check uses membership semantics so an SDK or OpenHands wrapper with its own non-404 status cannot erase an inner 404.
run_cogitate deliberately remains unmarked because the model call happens inside Conversation / Agent.astep with no seam we own that separates it from tool execution. Cloud tool 404s therefore stay out of model_not_found unless they also carry trusted SDK NotFound identity.
Validation note: full make ci on this reviewed tree reported two failures outside this diff: tests/test_observer_client_bundle.py::test_observer_client_bundle_old_fixture_tree_absence_and_residue_do_not_affect_outputs and solstone/apps/speakers/tests/test_routes.py::test_api_owner_bootstrap_full_loop_from_stubbed_labels. Both passed when run individually; attribution is under separate investigation, so this commit does not claim the full gate is green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ruff format only; no assertion text or logic changed. The two follow-up
commits on this branch were validated with focused test runs, so
format-check never gated them and the drift reached the ship gate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AC8 requires these glyphs to assert as still-present while they remain outside this lode.
L4 should move the observe pins to CONVERTED_GLYPH_RESIDUE when it converts them, exactly as L3 did with its four. meeting.py stays pinned because it is deferred outright; its glyphs go into stored journal markdown.
A record carrying both an emoji and a stranded non-Lucide glyph in icon is ambiguous; the migration now skips it byte-unchanged and logs, rather than dropping the glyph.
The read normalizer is unchanged. Preferring emoji for rendering is already correct, and a leftover non-Lucide icon resolves to no icon, so nothing breaks on disk.
The guard now covers the import/search/activity/settings slots added by L3, scans escaped Unicode before matching converted glyph residue, and avoids scanning workspace JavaScript while checking CSS rules. Cross-dict tests iterate SOURCE_METADATA, AGENT_ICONS, and DEFAULT_ACTIVITIES so reachable Lucide names resolve without adding any CONVEY_ICON_NAMES.
Import and search now use server-side icon_svg at every render site; CONVEY_ICON_NAMES stays unchanged with zero additions, and convey_icons.js plus emoji-lucide.json are untouched.
emoji is removed outright from SOURCE_METADATA and AGENT_ICONS because these are repo-owned constants we author with no other consumer. The arc's never-mutate-stored-emoji rule protects owner data, where Lucide translation is additive; deleting these repo-owned glyphs makes AC2 provable instead of approximate and should not be reverted under that rule.
The shared import-guide-title glyph is dropped from both the dynamic source heading and quick import because one class with two rules is worse than either. /api/sources now returns respond_collection, bringing it in line with the no-bare-top-level-array convention and updating all in-repo consumers.
Activity types now mirror the facet model: emoji remains the glyph fallback and icon is the Lucide name. Routes resolve server-side icon_svg for activities/settings surfaces, while clients render icon_svg || escaped emoji for owner-supplied data.
The read path normalizes a legacy custom record with a glyph sitting in icon in memory, so the maint task is cleanup and never a prerequisite. The write boundary still rejects an emoji passed as icon; only solstone.think.activities writes facets/*/activities/activities.jsonl during migration.
Rename the shared icon resolver now that activities use the same icon/emoji precedence as facets. This is a clean break with no alias: existing callers in shell data, settings facet payloads, and icon tests move to resolve_icon_svg.
Track notify positional-token presence separately from the joined message so sol notify "" emits an empty message like Python nargs=+ instead of raising a missing-message error.
Pin the empty-message parity vector, document the Unix socket timeout limitation at the send site, and assert the pre-dispatch notify help bytes stay tied to the handler output.
Port top-level sol notify to the native Rust surface with a NotificationSink seam, Unix socket delivery, parity notification capture, and no Python compatibility path.
Retire sol contract from the sol surface while preserving journal contract and the contract bundle builder. The top-level compat inventory shrinks from six commands to four: doctor, check, skills, link; the call journal prefix group remains the only subtree compat entry.
status now reconciles recorded intent metadata against live state for scout, spl, spb, and spp, making the previously unreachable fingerprint and identity residuals genuinely reachable while staying read-only and network-free.
SPB instance_id mismatch surfaces the named spb_instance_mismatch error code instead of collapsing into generic payload_invalid.
A malformed same-run intent is rejected as intent_malformed before any prepare side effect.
Envelope serialization enforces closed capability names and residual codes, failing loudly on an undeclared value.
Removed unused supported_contract_payload(); disable capability help derives from manifest.APPLY_CAPABILITIES so the manifest stays the single source.
Widened SPB fault injection to each mutation boundary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
logs header/controls wrap at narrow widths instead of pushing controls past the viewport edge
vitals severity label uses the pill's own white ink (was mid-grey, 1.00:1 on the error fill)
brain health heading joins the .card-title hierarchy with a UA margin reset
log stream "all" option lowercased to match its siblings
vitals chips constrain width and break long colon-delimited identifiers
three observe section titles lowercased to match the other three in the same grid
_css_rule in the app's style test is now line-anchored so base rules resolve instead of earlier collapsed-state overrides, and _hex_to_rgb expands shorthand hex.
Validation already recorded: make test-app APP=health 154 passed; make test-only TEST=tests/test_convey_icons.py 12 passed; make format-check clean.
The empty-state test asserted "icon" not in body / "action" not in body, which would false-fail if someone added a source comment in emptyStateHTML() explaining why there is no icon: the antenna glyph is absent from CONVEY_ICON_NAMES.
Asserting icon: / action: still catches a real option being added while ignoring prose.
emptyStateHTML() now returns window.SurfaceState.empty({...}) instead of a bare <h3 class="observer-empty-heading">, giving the empty state the house wrapper and a description sentence. The dead .observer-empty-heading CSS rule is removed.
headingLevel: 'h3' is passed deliberately: render() in solstone/convey/static/app.js falls back to h2 via normalizeHeadingLevel, and this page already owns <h2 class="section-title">devices</h2> while groupHeaderHTML() emits h3.
The icon is deliberately omitted: the rail glyph antenna is not in the browser-side CONVEY_ICON_NAMES allowlist, so it would silently render as nothing.
Two statsHTML() <dt> labels are lowercased to match their two siblings.
Update the skill-system docs to point humans at make skills while naming scripts/build_skill_references.py as the checked-in router reference generator and check target.
Relocate the router skill reference generator into scripts/build_skill_references.py as a standalone --check-capable build_* sibling generator. make skills and make check-skill-references now invoke the script directly.
The generated reference behavior stays byte-identical apart from the banner line. sol skills build still works from a source checkout through a repo-root-anchored lazy import, and the compatibility verb remains for follow-up removal.
The model_not_found readiness detail had an ambiguous pronoun and now names the key explicitly.
exception_chain and is_cloud_model_not_found join shared.__all__ now that they are imported cross-module.
docs/DOCTOR.md's agent-error triage commands are relabeled as journal-day queries, with the errored today recipe rewritten to glob every day index and filter by the local execution day so it matches what the product now reports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Solstone's context-window rescue was proven only against hand-constructed LLMBadRequestError instances, so upstream mapper drift could silently reroute recognised patterns to provider_request_rejected.
The new boundary test drives every _CONTEXT_WINDOW_PATTERNS entry through the real map_provider_exception and asserts each still classifies as context_window_exceeded. It also pins the openhands-sdk 1.27.1 wrapper split as an explicit named contract that fails with actionable guidance on drift.
A durable comment beside the rescue records why it must precede the permanent request-rejection fallback.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Format the talent-failure scan tests added in 17ed84025 so the full format gate stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The brain vocabulary, ingress allowlist, and validation path already knew model_not_found, but the shared runtime classifier could never produce it, so a definite cloud 404 degraded to unknown with no owner-facing readiness copy.
A single cloud-scoped predicate in providers/shared.py now recognises the NotFoundError-shaped exceptions the cloud SDKs actually raise; openhands._model_not_found's divergent copy is gone and _validation_reason reuses the shared rule.
model_not_found joins RUNTIME_REASON_CODES, gains setup-class readiness copy across the Python, JS, and native surfaces, and is capped at one deterministic failure. Bare 404-bearing exceptions and non-cloud providers keep their existing classification.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The no-argument scan previously read only journal/talents/<today>.jsonl, so a talent run that executed today against an older journal day failed invisibly. The default path now folds every ????????.jsonl day index into one stream, filters by the recorded epoch-ms execution timestamp against the local calendar day, and resolves across that filtered window.
Explicit-day scans keep journal-day semantics unchanged. shell_data._resolve_attention no longer falls through silently when the scan degrades.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fd78f4938 (lode A) correctly turned .chat-transcript .chat-event--talent from a row flex into a column flex to fix a real defect, but it paired that change with align-items: flex-start, which silently moved talent cards from horizontally centred to left-aligned.
On a column, align-items carries horizontal alignment. The justify-content: center inherited from app.css was the row-era centring authority and is now a dead main-axis declaration on an auto-height column, so align-items had to be the fix.
.chat-error-block is the alignment peer being matched: .chat-event--error is still a row and still centres, so the two peer card types visually disagreed inside a single transcript.
center also centres the thinking expander and thinking content, because a talent_finished with thinking yields three direct flex children of .chat-event--talent: the card, the .chat-thinking-expander button, and the .chat-thinking-content div, since fragments flatten on append. For a card without thinking, center restores the pre-regression position; for the thinking case it is a deliberate new layout decision, not a restoration, since the pre-regression state there was lode A's original bug with the expander beside the card.
flex-direction: column is untouched. It is lode A's real fix and stays.
Detail indicators are joined into a single innerHTML string, so the container's flex layout previously held one anonymous text item and the ' · ' separators did the spacing.
Injecting the icon element split that run into separate flex items and added 1em gaps around the glyph. Switch the container to block and override the icon svg to inline-block in that context only, leaving the card, facet, button, and delete contexts on the shared rule.
Assert L2 slot CSS uses currentColor stroke sizing and guard the converted call idiom.
Preserve accessibility, escaping, glyph residue, and trust-line text expectations for the converted sites.
Render the L2 control and health trust icons with vendored Lucide SVGs and currentColor styling.
Commit solstone/convey/static/convey_icons.js as generated output from python -m scripts.build_convey_icons.
Accept matched quote forms and optional chaining when extracting ConveyIcons calls.
Include vouched inline slot SVGs in the used-name set so static Lucide slots count toward guard coverage.
The mobile .app-bar inset was declared 1,183 lines above the unconditional rule at equal specificity, so it never applied — the bar sat inset 60px for a menu rail that is width: 0 at that breakpoint and flush against the right edge. Moves the inset below the unconditional rule, following the idiom already documented at app.css:1118-1121, and deletes the dead rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Unbreakable sol://… tokens in reflection prose pushed the document to 396px against a 375px viewport. Scoped to inline code so prose still wraps on word boundaries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A missing weekly reflection rejected with a 404 that the client routed into the generic error surface, inviting a support report about a file that never existed. The server now attaches owner-facing copy to the missing-file branch only; the client renders SurfaceState.empty when the 404 carries that copy and otherwise still reaches the retry/report error surface. Status code and the unparseable-token error path are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Short activities rendered 8-10px tall against a 12px line box, slicing the label; element opacity also washed the white label toward the ground at ~1.3:1. Adds a min-height floor on base .occ (so the mobile 30px touch-target floor still wins by source order), moves the tint into the fill alpha at the day-view emission site, sets dark ink, and re-expresses hover/active as a filter step. levelColor()'s return value is unchanged, so activity-card accent rails are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.agent-card-title is a flex item with no min-width: 0, so its automatic minimum size was min-content; a long unbreakable title overflowed the header and pushed .agent-card-badges outside the card.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The talent declared neither title nor description, so _build_talents_meta fell back to the raw key and rendered timeline:segment_summary as a card title. Adds the two frontmatter values, updates the three baselines that dumped the fallback, and adds a class guard so a future talent without a title fails CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fix talent chat events to lay out as a column so the thinking expander stacks under the card instead of becoming a row sibling slab.
Fix hidden origin provenance collapse by adding a scoped rule that restores display: none where app.css's display: flex was defeating the hidden attribute.
Skip empty provenance values so dangling dedupe/since labels are not rendered, and format since_ts as a UTC-pinned timestamp instead of a raw epoch-ms integer.
Lowercase nine talent labels plus the queued label to match the convey chat surface.
solstone/think/native/chat/command.rs:515-521 and
core/fixtures/native-sol/parity/chat.jsonl:15 still carry the Capitalized
"Making that change…" / "Reaching solstone support…" literals. They are the native
`sol chat` terminal surface, not the convey web canon this change enforces, and are
out of scope here — editing them pulls in docs/PORTING.md lockstep rules and a paired
fixture edit. Left untouched deliberately; follow-up.
Anchor icon-slot detection on the slot element instead of the SVG line so multi-line surface-state-icon and SVG markup is no longer skipped; add a fixture test for that shape.
Bind error-wiring assertions from each selector lookup to its handler: support retry to loadTickets(deps), sol retry to loadTalents(), and support secondary to the back-to-tickets restore.
Keep the emptyIcons map branch deliberately as a regression tripwire for the retired pattern.
Replace emoji and hand-copied empty-state icons with SurfaceState plus window.ConveyIcons.svg(...) across converted app surfaces.
Delete the old emptyIcons maps and orphaned empty-state CSS without fallbacks, and wire retry/secondary error handlers explicitly.
Exercise the generated accessor in node, compare shipped SVG against lucide.json, and require converted call sites to use allow-listed names.
Scan converted app files for unvouched icon-slot SVGs while preserving the out-of-scope glyphs.
Generate window.ConveyIcons from lucide.json with CONVEY_ICON_NAMES as the single allow-list.
Load the generated accessor from the shell and move the SurfaceState example beside its API.
The cloud lane raises openhands.sdk.llm.exceptions.types.LLMBadRequestError, not litellm.exceptions.BadRequestError, so real Gemini 400s recorded unknown; the wrapper carries no status_code.
The match folds into the existing cloud-gated rejection arm as a name-equality condition, kept below the _CONTEXT_WINDOW_PATTERNS message check so an LLMBadRequestError carrying a context-overflow message still classifies as context_window_exceeded (cap 2, retryable) rather than being mislabeled a defect in sol. The litellm status-code condition stays reachable in the same arm.
The drafted bullet said sol now names a refused provider request plainly
instead of showing an internal code. Verified against this tree, that is false
on the cloud lane, which is the audience the adjacent google bullet addresses.
classify_provider_error gates provider_request_rejected on a litellm.exceptions
BadRequestError with status 400. The cloud path never reaches it: openhands
LLMBadRequestError is not a subclass of the litellm type, and more decisively
_handle_agent_error_event rebuilds a synthetic RuntimeError from the event
string before classifying, so the original type is gone. Every realistic cloud
400 message classifies as unknown, confirmed by direct invocation.
The registration commits stay in the release; they simply have no owner-visible
effect on the cloud lane yet, so they are correctly unnarrated. A lode is in
flight to match the wrapper the cloud lane actually raises; the claim can ship
once that lands and is verified.
find_blockers walked the filesystem with rglob and skipped only a hardcoded
SKIP_DIRS that omitted scratch/, whose contents are git-ignored local working
space (only scratch/.gitignore is tracked). Agent session transcripts land
there and quote historical pyproject.toml dependency lists verbatim, so eight
scratch files reddened the canonical release gate on a developer clone while
the shipped source carried zero references. The guard also reported RETAIN
rather than delete-safe on that basis, so the noise produced a wrong readiness
verdict, not just a red.
Add scratch to SKIP_DIRS and cover the exclusion with a falsification test: a
scratch transcript alone yields no blockers, and a real caller placed beside it
still blocks. The test fails without the scope change.
Found by the 1.0.16 release train; git grep over tracked files confirmed the
only non-scratch matches are the three existing ALLOWED_PATHS.
Cut the release metadata to 1.0.16 from the root project version and 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.
CHANGELOG block drafted via the CMO changelog sub-agent recipe. Sensitive-content gate applied and cleared by the founder in-session for the three security-adjacent bullets: LAN pairing certificate delivery, direct pair-link address admission, and the google provider schema change.
_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.
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.
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.
`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.
Cut from the root project version and propagated derived packaging state via
scripts/render_packaging.py and uv lock; no hand-edited derived files.
1.0.17 exists to carry 5a803f789, the setup access-surface fix. 1.0.16 shipped a
journal setup that failed two of its own steps and exited non-zero, which hangs
the macOS app first-run onboarding. The changelog bullet names the owner-visible
effect on both surfaces: a setup that reported a problem when nothing was wrong,
and a new Mac install that could wait forever at the last step.
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.
The batch-3 fix (0dbcb0d67) added a 12px horizontal component to .link-modal's padding shorthand. That component was cosmetic.
It narrowed .link-modal-box (width: 92%) from 331px to 309px at a 375px viewport, which produced 9px of internal horizontal overflow (scrollWidth 318 vs clientWidth 309) because the QR panel is effectively fixed at ~294px and was being clipped.
Zeroing only the horizontal component restores the box to 331px with scrollWidth == clientWidth, and the QR panel fits inside the box.
.link-modal-box { width: 92% } supplies the side gutter on its own; at every viewport that 4%-per-side gutter is at least as wide as the 12px removed.
The vertical components calc(var(--facet-bar-height) + 12px) and calc(var(--app-bar-height) + 80px) are unchanged; they keep #link-pair-cancel clear of the fixed chat bar.
Constrain `.link-modal` to the band between the facet bar and chat bar and let `.link-modal-box` scroll internally, so `#link-pair-cancel` is no longer painted under the fixed chat bar.
Cap the `is-glasses` QR at `min(100%, 360px)` instead of `100%`, so the modal no longer overflows the viewport.
Add `DEVICE_LABEL_PLACEHOLDER_COMPUTER` and set the `#link-device-label` placeholder from the selected presentation mode in `setPairCopy()`. The placeholder seam runs before a pair request exists because `setPresentationMode()` calls `setPairCopy()` ahead of `renderPairPresentation()` and its `!currentPair` guard.
Co-Authored-By: OpenAI Codex <codex@openai.com>
The mixed-writability hazard test previously detected root and returned before asserting anything, so under root execution, which is live in this repo, AC 12 had zero coverage while showing green.
Add a root-proof variant that places a regular file at one agent's skills-root path. mkdir through a non-directory component fails with ENOTDIR for every uid including root, so the vector runs everywhere with no chmod and no seam. It asserts the writable agent still installs, that an error row names the unwritable agent and path, and exit status 1. The row reason is deliberately not byte-pinned, because permission failures use Rust-native io::Error text under the divergence recorded in 9663b6445.
Retain the chmod-based test for the true permission-bits case, and make its root skip loud rather than silent so a root run reports the gap instead of showing green.
The negative control was verified: temporarily inverting the new test's exit assertion made it fail with exit 101 and left: 1 right: 0, confirming the test can actually fail.
Remove a redundant !is_symlink() clause from a symlink_metadata-sourced check in the tree walk, where is_dir() is already false for symlinks.
Co-Authored-By: OpenAI Codex <codex@openai.com>
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>
sol skills install, uninstall, and list now run through a direct match arm in solstone_core_sol::run, placed before the compatibility dispatcher. It is not a native authority entry: the inventory builder globs solstone/**/native/**/authority.toml, every entry lands in a partition with a frozen count, and sol-call paths must exist in the frozen oracle grammar. skills is a top-level non-call verb with no oracle path.
Remove skills from TOP_LEVEL_COMPAT_MODULES and TOP_LEVEL_COMPAT_COMMANDS, one element each. The compatibility dispatcher, the SOLSTONE_NATIVE_COMPAT_ACTIVE sentinel, and every other compat entry are untouched. A new order-insensitive, count-free element-identity assertion ties the two collections together so they cannot drift.
One Python/Rust divergence is knowingly accepted: install_user does not reproduce the os.access(target, W_OK) pre-check. access(2) has no std equivalent and adding libc to reproduce one error string is not warranted. The filesystem outcome is identical: a mode-0500 directory fails remove_dir_all immediately with no partial deletion. Only the reason string differs, and it is Rust-native. AC 12 is covered structurally by a hazard test rather than byte-pinned.
The project root is canonicalized inside the skills path only, matching Python's get_project_root() realpath so lexical relpath targets agree. resolve_project_root(), sol root output, and check-native-sol-root-contract are unchanged.
skills.rs is added to CORE_REQUIRED_SDIST_MEMBERS in scripts/check_wheel_contents.py because a new solstone-core-sol source file must ship in the core sdist.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Capture 30 parity vectors by running the live Python oracle in solstone/think/skills_cli.py through scripts/capture_skills_parity_oracle.py. These vectors are generated evidence, not hand-written fixtures.
This lands before that Python module is deleted later in this lode, so git log over core/fixtures/native-sol/skills-parity-v1/ proves provenance. A fixture set reconstructed by reading deleted code would not be evidence.
The harness deliberately has no --check mode and is not wired into make ci. Its oracle is deleted two commits later, so such a gate would be permanently red; this is a deliberate divergence from the scripts/build_journal_resolution_vectors.py precedent.
stdout and stderr are captured as separate streams. argparse's own usage/help/error message bodies and Python OSError.__str__ errno renderings are deliberately not pinned; for the exit-2 usage vectors only the exit status is asserted.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Drop has_cloud_model_request_mark from shared.py::__all__. Its only caller is is_cloud_model_not_found in the same module, and the repo has no star-imports from providers.shared, so exporting it added dead public surface without a consumer.
Keep the chain-wide model-request marker, but document why it marks every exception in the chain: OpenHands _unwrap_provider_exception classifies __cause__ or __context__, so a mark on only the caught wrapper could be discarded before classification.
Correct docs/design/openapi-client-contract.md for the RUNTIME_REASON_CODES line shift introduced by this branch.
Validation record: the full make ci gate passed on the preceding tree with 15193 passed, 17 skipped, and Rust checks included. The two failures recorded in 270a13d11's message were subsequently attributed to an intermittent full-suite isolation issue with no causal path to this diff; those tests depend on observer-bundle temp-root hashing and speakers temp-journal route state, neither of which this branch touches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Specialize model_not_found copy only in the dynamic provider readiness view, where provider and model are both available. When a model id is present, the visible summary and detail now name that selected model; when model is falsy, the existing _ENTRIES text is rendered unchanged.
The static registry stays generic because _ENTRIES projects verbatim to chat_reasons.js and command.rs::readiness_summary, and those model-less surfaces do not have model identity. Adding a {model} token there would leak an unresolved placeholder into static projections instead of improving the runtime view.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stamp model-request provenance only around the OpenHands llm.responses / llm.completion transport invocations and their async equivalents. Those helpers are reached by built-in cloud generate and the validation probe; local and BYO endpoints route through local.py instead, so they never receive this marker.
A generic 404 now becomes model_not_found only when the marker is present in the exception chain and some chain item reports status 404. Trusted provider SDK NotFound-shaped classes keep the identity path, constrained to litellm.exceptions, openai, and anthropic namespaces, so lookalike local classes remain unknown. The chain status check uses membership semantics so an SDK or OpenHands wrapper with its own non-404 status cannot erase an inner 404.
run_cogitate deliberately remains unmarked because the model call happens inside Conversation / Agent.astep with no seam we own that separates it from tool execution. Cloud tool 404s therefore stay out of model_not_found unless they also carry trusted SDK NotFound identity.
Validation note: full make ci on this reviewed tree reported two failures outside this diff: tests/test_observer_client_bundle.py::test_observer_client_bundle_old_fixture_tree_absence_and_residue_do_not_affect_outputs and solstone/apps/speakers/tests/test_routes.py::test_api_owner_bootstrap_full_loop_from_stubbed_labels. Both passed when run individually; attribution is under separate investigation, so this commit does not claim the full gate is green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AC8 requires these glyphs to assert as still-present while they remain outside this lode.
L4 should move the observe pins to CONVERTED_GLYPH_RESIDUE when it converts them, exactly as L3 did with its four. meeting.py stays pinned because it is deferred outright; its glyphs go into stored journal markdown.
A record carrying both an emoji and a stranded non-Lucide glyph in icon is ambiguous; the migration now skips it byte-unchanged and logs, rather than dropping the glyph.
The read normalizer is unchanged. Preferring emoji for rendering is already correct, and a leftover non-Lucide icon resolves to no icon, so nothing breaks on disk.
The guard now covers the import/search/activity/settings slots added by L3, scans escaped Unicode before matching converted glyph residue, and avoids scanning workspace JavaScript while checking CSS rules. Cross-dict tests iterate SOURCE_METADATA, AGENT_ICONS, and DEFAULT_ACTIVITIES so reachable Lucide names resolve without adding any CONVEY_ICON_NAMES.
Import and search now use server-side icon_svg at every render site; CONVEY_ICON_NAMES stays unchanged with zero additions, and convey_icons.js plus emoji-lucide.json are untouched.
emoji is removed outright from SOURCE_METADATA and AGENT_ICONS because these are repo-owned constants we author with no other consumer. The arc's never-mutate-stored-emoji rule protects owner data, where Lucide translation is additive; deleting these repo-owned glyphs makes AC2 provable instead of approximate and should not be reverted under that rule.
The shared import-guide-title glyph is dropped from both the dynamic source heading and quick import because one class with two rules is worse than either. /api/sources now returns respond_collection, bringing it in line with the no-bare-top-level-array convention and updating all in-repo consumers.
Activity types now mirror the facet model: emoji remains the glyph fallback and icon is the Lucide name. Routes resolve server-side icon_svg for activities/settings surfaces, while clients render icon_svg || escaped emoji for owner-supplied data.
The read path normalizes a legacy custom record with a glyph sitting in icon in memory, so the maint task is cleanup and never a prerequisite. The write boundary still rejects an emoji passed as icon; only solstone.think.activities writes facets/*/activities/activities.jsonl during migration.
Track notify positional-token presence separately from the joined message so sol notify "" emits an empty message like Python nargs=+ instead of raising a missing-message error.
Pin the empty-message parity vector, document the Unix socket timeout limitation at the send site, and assert the pre-dispatch notify help bytes stay tied to the handler output.
Port top-level sol notify to the native Rust surface with a NotificationSink seam, Unix socket delivery, parity notification capture, and no Python compatibility path.
Retire sol contract from the sol surface while preserving journal contract and the contract bundle builder. The top-level compat inventory shrinks from six commands to four: doctor, check, skills, link; the call journal prefix group remains the only subtree compat entry.
status now reconciles recorded intent metadata against live state for scout, spl, spb, and spp, making the previously unreachable fingerprint and identity residuals genuinely reachable while staying read-only and network-free.
SPB instance_id mismatch surfaces the named spb_instance_mismatch error code instead of collapsing into generic payload_invalid.
A malformed same-run intent is rejected as intent_malformed before any prepare side effect.
Envelope serialization enforces closed capability names and residual codes, failing loudly on an undeclared value.
Removed unused supported_contract_payload(); disable capability help derives from manifest.APPLY_CAPABILITIES so the manifest stays the single source.
Widened SPB fault injection to each mutation boundary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
logs header/controls wrap at narrow widths instead of pushing controls past the viewport edge
vitals severity label uses the pill's own white ink (was mid-grey, 1.00:1 on the error fill)
brain health heading joins the .card-title hierarchy with a UA margin reset
log stream "all" option lowercased to match its siblings
vitals chips constrain width and break long colon-delimited identifiers
three observe section titles lowercased to match the other three in the same grid
_css_rule in the app's style test is now line-anchored so base rules resolve instead of earlier collapsed-state overrides, and _hex_to_rgb expands shorthand hex.
Validation already recorded: make test-app APP=health 154 passed; make test-only TEST=tests/test_convey_icons.py 12 passed; make format-check clean.
The empty-state test asserted "icon" not in body / "action" not in body, which would false-fail if someone added a source comment in emptyStateHTML() explaining why there is no icon: the antenna glyph is absent from CONVEY_ICON_NAMES.
Asserting icon: / action: still catches a real option being added while ignoring prose.
emptyStateHTML() now returns window.SurfaceState.empty({...}) instead of a bare <h3 class="observer-empty-heading">, giving the empty state the house wrapper and a description sentence. The dead .observer-empty-heading CSS rule is removed.
headingLevel: 'h3' is passed deliberately: render() in solstone/convey/static/app.js falls back to h2 via normalizeHeadingLevel, and this page already owns <h2 class="section-title">devices</h2> while groupHeaderHTML() emits h3.
The icon is deliberately omitted: the rail glyph antenna is not in the browser-side CONVEY_ICON_NAMES allowlist, so it would silently render as nothing.
Two statsHTML() <dt> labels are lowercased to match their two siblings.
Relocate the router skill reference generator into scripts/build_skill_references.py as a standalone --check-capable build_* sibling generator. make skills and make check-skill-references now invoke the script directly.
The generated reference behavior stays byte-identical apart from the banner line. sol skills build still works from a source checkout through a repo-root-anchored lazy import, and the compatibility verb remains for follow-up removal.
The model_not_found readiness detail had an ambiguous pronoun and now names the key explicitly.
exception_chain and is_cloud_model_not_found join shared.__all__ now that they are imported cross-module.
docs/DOCTOR.md's agent-error triage commands are relabeled as journal-day queries, with the errored today recipe rewritten to glob every day index and filter by the local execution day so it matches what the product now reports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Solstone's context-window rescue was proven only against hand-constructed LLMBadRequestError instances, so upstream mapper drift could silently reroute recognised patterns to provider_request_rejected.
The new boundary test drives every _CONTEXT_WINDOW_PATTERNS entry through the real map_provider_exception and asserts each still classifies as context_window_exceeded. It also pins the openhands-sdk 1.27.1 wrapper split as an explicit named contract that fails with actionable guidance on drift.
A durable comment beside the rescue records why it must precede the permanent request-rejection fallback.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The brain vocabulary, ingress allowlist, and validation path already knew model_not_found, but the shared runtime classifier could never produce it, so a definite cloud 404 degraded to unknown with no owner-facing readiness copy.
A single cloud-scoped predicate in providers/shared.py now recognises the NotFoundError-shaped exceptions the cloud SDKs actually raise; openhands._model_not_found's divergent copy is gone and _validation_reason reuses the shared rule.
model_not_found joins RUNTIME_REASON_CODES, gains setup-class readiness copy across the Python, JS, and native surfaces, and is capped at one deterministic failure. Bare 404-bearing exceptions and non-cloud providers keep their existing classification.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The no-argument scan previously read only journal/talents/<today>.jsonl, so a talent run that executed today against an older journal day failed invisibly. The default path now folds every ????????.jsonl day index into one stream, filters by the recorded epoch-ms execution timestamp against the local calendar day, and resolves across that filtered window.
Explicit-day scans keep journal-day semantics unchanged. shell_data._resolve_attention no longer falls through silently when the scan degrades.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fd78f4938 (lode A) correctly turned .chat-transcript .chat-event--talent from a row flex into a column flex to fix a real defect, but it paired that change with align-items: flex-start, which silently moved talent cards from horizontally centred to left-aligned.
On a column, align-items carries horizontal alignment. The justify-content: center inherited from app.css was the row-era centring authority and is now a dead main-axis declaration on an auto-height column, so align-items had to be the fix.
.chat-error-block is the alignment peer being matched: .chat-event--error is still a row and still centres, so the two peer card types visually disagreed inside a single transcript.
center also centres the thinking expander and thinking content, because a talent_finished with thinking yields three direct flex children of .chat-event--talent: the card, the .chat-thinking-expander button, and the .chat-thinking-content div, since fragments flatten on append. For a card without thinking, center restores the pre-regression position; for the thinking case it is a deliberate new layout decision, not a restoration, since the pre-regression state there was lode A's original bug with the expander beside the card.
flex-direction: column is untouched. It is lode A's real fix and stays.
Detail indicators are joined into a single innerHTML string, so the container's flex layout previously held one anonymous text item and the ' · ' separators did the spacing.
Injecting the icon element split that run into separate flex items and added 1em gaps around the glyph. Switch the container to block and override the icon svg to inline-block in that context only, leaving the card, facet, button, and delete contexts on the shared rule.
The mobile .app-bar inset was declared 1,183 lines above the unconditional rule at equal specificity, so it never applied — the bar sat inset 60px for a menu rail that is width: 0 at that breakpoint and flush against the right edge. Moves the inset below the unconditional rule, following the idiom already documented at app.css:1118-1121, and deletes the dead rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A missing weekly reflection rejected with a 404 that the client routed into the generic error surface, inviting a support report about a file that never existed. The server now attaches owner-facing copy to the missing-file branch only; the client renders SurfaceState.empty when the 404 carries that copy and otherwise still reaches the retry/report error surface. Status code and the unparseable-token error path are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Short activities rendered 8-10px tall against a 12px line box, slicing the label; element opacity also washed the white label toward the ground at ~1.3:1. Adds a min-height floor on base .occ (so the mobile 30px touch-target floor still wins by source order), moves the tint into the fill alpha at the day-view emission site, sets dark ink, and re-expresses hover/active as a filter step. levelColor()'s return value is unchanged, so activity-card accent rails are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The talent declared neither title nor description, so _build_talents_meta fell back to the raw key and rendered timeline:segment_summary as a card title. Adds the two frontmatter values, updates the three baselines that dumped the fallback, and adds a class guard so a future talent without a title fails CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fix talent chat events to lay out as a column so the thinking expander stacks under the card instead of becoming a row sibling slab.
Fix hidden origin provenance collapse by adding a scoped rule that restores display: none where app.css's display: flex was defeating the hidden attribute.
Skip empty provenance values so dangling dedupe/since labels are not rendered, and format since_ts as a UTC-pinned timestamp instead of a raw epoch-ms integer.
Lowercase nine talent labels plus the queued label to match the convey chat surface.
solstone/think/native/chat/command.rs:515-521 and
core/fixtures/native-sol/parity/chat.jsonl:15 still carry the Capitalized
"Making that change…" / "Reaching solstone support…" literals. They are the native
`sol chat` terminal surface, not the convey web canon this change enforces, and are
out of scope here — editing them pulls in docs/PORTING.md lockstep rules and a paired
fixture edit. Left untouched deliberately; follow-up.
Anchor icon-slot detection on the slot element instead of the SVG line so multi-line surface-state-icon and SVG markup is no longer skipped; add a fixture test for that shape.
Bind error-wiring assertions from each selector lookup to its handler: support retry to loadTickets(deps), sol retry to loadTalents(), and support secondary to the back-to-tickets restore.
Keep the emptyIcons map branch deliberately as a regression tripwire for the retired pattern.
The cloud lane raises openhands.sdk.llm.exceptions.types.LLMBadRequestError, not litellm.exceptions.BadRequestError, so real Gemini 400s recorded unknown; the wrapper carries no status_code.
The match folds into the existing cloud-gated rejection arm as a name-equality condition, kept below the _CONTEXT_WINDOW_PATTERNS message check so an LLMBadRequestError carrying a context-overflow message still classifies as context_window_exceeded (cap 2, retryable) rather than being mislabeled a defect in sol. The litellm status-code condition stays reachable in the same arm.
The drafted bullet said sol now names a refused provider request plainly
instead of showing an internal code. Verified against this tree, that is false
on the cloud lane, which is the audience the adjacent google bullet addresses.
classify_provider_error gates provider_request_rejected on a litellm.exceptions
BadRequestError with status 400. The cloud path never reaches it: openhands
LLMBadRequestError is not a subclass of the litellm type, and more decisively
_handle_agent_error_event rebuilds a synthetic RuntimeError from the event
string before classifying, so the original type is gone. Every realistic cloud
400 message classifies as unknown, confirmed by direct invocation.
The registration commits stay in the release; they simply have no owner-visible
effect on the cloud lane yet, so they are correctly unnarrated. A lode is in
flight to match the wrapper the cloud lane actually raises; the claim can ship
once that lands and is verified.
find_blockers walked the filesystem with rglob and skipped only a hardcoded
SKIP_DIRS that omitted scratch/, whose contents are git-ignored local working
space (only scratch/.gitignore is tracked). Agent session transcripts land
there and quote historical pyproject.toml dependency lists verbatim, so eight
scratch files reddened the canonical release gate on a developer clone while
the shipped source carried zero references. The guard also reported RETAIN
rather than delete-safe on that basis, so the noise produced a wrong readiness
verdict, not just a red.
Add scratch to SKIP_DIRS and cover the exclusion with a falsification test: a
scratch transcript alone yields no blockers, and a real caller placed beside it
still blocks. The test fails without the scope change.
Found by the 1.0.16 release train; git grep over tracked files confirmed the
only non-scratch matches are the three existing ALLOWED_PATHS.
Cut the release metadata to 1.0.16 from the root project version and 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.
CHANGELOG block drafted via the CMO changelog sub-agent recipe. Sensitive-content gate applied and cleared by the founder in-session for the three security-adjacent bullets: LAN pairing certificate delivery, direct pair-link address admission, and the google provider schema change.