dcdbcb8a2b14a5dfd35ca141237d3c7d3715046b
892 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ee2f5629b8 |
Desktop connect runs on the connection operation: one card, no link to the model, no renderer polling (NS-868) (#110574)
* refactor(connectors): cut comments that restate the code
Connector modules (tools/connectors, tui_gateway connector RPCs, desktop
connector card/store) keep only comments that carry a non-derivable why or
a cross-module contract. No behaviour change.
* feat(connectors): managed connect runs on the connection operation
Managed `connect` / `reconnect` mint one ConnectionOperation for every target and, on a
desktop session, block the tool turn until the operation settles; the result is per-target
outcomes and never carries a connect link. Off the desktop the result carries the links and
returns at once (PR3 delivers them as their own message).
Why: the previous leg handed the model a URL and a `wait` verb, and the renderer ran its own
2s poller on top of the backend's 5s one; both walked the whole gateway catalog at two vendor
calls per page to read one row (~3 Composio calls/s per pending target). A hidden composer
message started the model's `wait` on the user's behalf. None of it was observable from the
operation the MCP leg already used.
What the operation looks like now:
- `contract.py`: TargetState / Actor / SettleReason enums and the `(kind, from) -> {to: actor}`
transition table. `operation.transition()` enforces it; a card cannot claim a managed
target `connected`, only the backend watcher can.
- `live.py`: one open operation per session, found by `op_id`. `connectors.operation.status`
reads it, `connection.respond` drives it, `pending_connection` on resume replays it.
- `run.py`: the one lifecycle for both target kinds (prepare -> card -> wake/observe loop ->
settle -> result). The managed `observe` hook polls the gateway list once per tick for the
whole operation; the exact-status route replaces that call when the gateway ships it.
- `connection.update` is emitted on every transition and on settlement; registered in the
shared event contract with the operation vocabulary typed on the TS side.
- `wait`, `_rendered_links`, `_seen_instructions`, the just-minted bounce and `_clamp_timeout`
are deleted. `force` on `reconnect` always reinitiates; plain `reconnect` repairs only what
the gateway reports disconnected.
- `connections.wait_timeout_seconds` is removed from config defaults, the example and the
docs. The deadline is `OPERATION_DEADLINE_SECONDS = 300` in `operation.py`; the key was
added on this unmerged train so no migration is needed.
- Wire model: `statusReason` parsed on connection results; the seven-state `connectionStatus`
is typed on list items and an unknown value fails validation; `CONNECTION_REQUIRED` carries
`connect_card_available` instead of the link when the session platform is `desktop`.
Session platform, not callback presence, decides whether a card exists: the GUI bridge
attaches callbacks to every backend session, terminal TUI included.
* feat(desktop): connector card subscribes to the connection operation
The card renders from the backend's operation instead of driving its own: `connector-flow.ts`
(the renderer's 2s `connectors.list` poller, its 120s client deadline and `keepWaiting`) is
deleted, and both hidden composer submits in `connector-tool.tsx` go with it. The model is
never nudged into a `wait`; the tool call is blocked on the backend until the operation
settles.
- `connection-request.ts` is the operation store: keyed by `op_id`, one entry per session,
`applyOperationStatus` / `applyConnectionUpdate` as pure reducers, `respond` leaves the
entry in place (the backend answers with `connection.update`), `ConnectionTargetOutcome`
is a discriminated union the backend's transition table accepts.
- `input-requests.ts` applies `connection.update`; `connection.expire` and the resume
snapshot correlate by `op_id` (a snapshot has no `request_id`).
- `ConnectorOffer` renders one `ConnectorCard` per target from a single
`Record<ConnectionTargetState, phase>` table; Connect opens the stored link, Try again on
failed / expired reissues through `connectors.connect` on the open operation, Not now is a
per-target `skipped`, Continue settles. A settled operation renders `ConnectorSummary` rows
with no live control.
- `tool-render-class.ts`: `manage_connections` renders the card regardless of
`HERMES_GUEST_ONBOARDING`; the flag still gates the onboarding flow, not the card. The
backend gate already decided admission; a card only exists because the tool was admitted.
- `mcp-setup-tool.tsx` speaks the same outcome vocabulary (connected / skipped / failed).
- `ConnectorRow.connectionStatus` is the seven-state literal union, not `string | null`.
- The guided-onboarding poller (`first-build-connectors.ts`) keeps its own row/phase types
and compiles unchanged; PR3 moves it onto the operation.
anti-slop: no net-new findings (17 touched files vs 11d1a12472).
* fix(connectors): the card never parks the tool thread; every update carries the snapshot
Found by the pre-PR adversarial review and a real-path E2E test (both left in the tree).
- The desktop `connection_callback` was still `_block("connection.request", ...)`, which parked
the tool thread on a private request-id Event until a `_respond` that no longer exists for
this event. `connection.respond` settled the operation but the tool waited its full deadline
before the watcher loop even started. The callback now only emits the card; the operation's
own wake loop is the wait. The MCP leg's blocking bridge goes with it: the card answers
through `connection.respond` like every other card.
- `connection.request` and every `connection.update` frame carry the full target snapshot
(state, link, detail). The initial mint happened before the card existed, so the renderer
never saw the links and Connect stayed disabled; a Continue settlement stamped
`not_connected` on the backend while the card still showed `initiated`. The store now
overlays the snapshot; no state is reconstructed from deltas.
- The `connection.update` emitter is a class-level `on_change` slot on the operation, set
once by `register()` (a second `register()` no longer stacks wrappers); session lookup takes
`_sessions_lock`; a re-minted link on an `initiated` target goes through `refresh_link()`
and emits, instead of a bare attribute write.
- `session.interrupt` is checked before the first observe, so an interrupted call settles
`interrupt`, not `all_resolved`.
- A gateway list reporting `expired` for an initiated target is recorded with actor `clock`
(the contract's owner of that edge); it raised `IllegalTransition` before.
- Dead `keepWaiting` i18n keys from the deleted renderer poller removed.
tests/tui_gateway/test_connector_operation_e2e.py runs the desktop lifecycle through the real
tool, registry, gateway RPC handlers and callback bridge with only the HTTP client faked.
* docs(connectors): prompts and docs describe the operation, not the deleted wait verb
The onboarding prompts told the model to call action="wait" with timeout_seconds and to
expect a hidden [setup]/[connectors] note; both are gone. tool-search.md and
toolsets-reference.md said the model gets a connect link on the desktop. tui_gateway/AGENTS.md
gains the connection-operation row of the surface table.
* fix(connectors): the panel re-mints only a dead link
Try again on a failed or expired target mints a fresh link on the open operation. A waiting
target keeps the link it was minted with; the card reopens it and connectors.connect refuses
to spend a second mint (LINK_STILL_VALID). The unused refresh_link() goes. The package
docstring names the new siblings; the nine-name public surface is unchanged.
* test(connectors): the local-batch test answers the operation the way the card does
The callback stopped returning an answer in f782b26d98 (the card answers through
connection.respond); this test still returned one and waited out the 300s deadline in CI.
* ci: retrigger
* fix(connectors): the desktop card appears outside guided onboarding
Live on a signed-in macOS desktop, the two-app connect never showed a card. Three
defects, each hidden by a test that bound state the running app never binds.
The backend read the surface from HERMES_SESSION_PLATFORM only. The desktop and TUI
gateway bind it as HERMES_SESSION_SOURCE (_set_session_context), so session_platform()
was "" and managed connects took the off-desktop branch: links in the model's message,
no operation. session_platform() now reads platform, then source. The E2E test binds
through server._set_session_context instead of set_session_vars(platform="desktop").
The renderer routed manage_connections to the card only under isOnboardingEnabled(),
the HERMES_GUEST_ONBOARDING launch flag, in message-parts.tsx and the run splitter in
fallback.tsx. tool-render-class.ts had already dropped that gate in this PR; the two
routers had not. Both now route on the tool name alone.
ConnectorTool resolved the session owner by the runtime id. Owner routes, hints and
session rows are keyed by the stored id, so in registry topology the owner never
resolved and the card rendered null while the tool blocked. It now resolves by the
stored id, matching the PR1.5 card and every other owner lookup.
message-parts-connectors.test.tsx mounts the real Fallback router with the onboarding
flag off and distinct runtime/stored ids; red before each renderer fix, green after.
* style(connectors): shorter comments, no module mock in the card router test
The router test mocked isOnboardingEnabled to false; jsdom has no preload bridge, so the
real function already returns false. Comments that restated the code are cut to one line.
anti-slop: no net-new findings (25 touched files)
* fix(connectors): Connect on a waiting row opens the stored link
ConnectorCard derived the button's loading state from the phase label, so a managed row that
read "Finish connecting in your browser" (every row, since links are minted up front) had a
disabled Connect button. Nothing on the desktop could open the sign-in link; every managed
connect ended skipped, not_connected, or at the deadline.
The card now takes `busy` for "the action itself is running" and keeps `phase` as a label.
The MCP card passes its in-flight flag; the connector card passes the re-mint wait. Red before:
the Connect button on an initiated row rendered disabled and a click opened nothing.
* fix(connectors): a settled card stays dead; the card binds to its tool call only
A second connect for the same apps revived the finished card on the old tool row. The
connection.request payload carried no id, so the renderer fell back to matching rows by
connector names, and any row with those names qualified, settled or not.
The operation now records the model's tool_call_id and sends it in connection.request and in
the resume snapshot. The card binds to the tool row with that id and to nothing else; the
name-match fallback is deleted. A payload without the id is rejected by the store.
`reason` is removed from the tool: it was the only text the card ever showed from the model
and its absence forked a second tool part, since `reason` doubled as the row-correlation key
in tool-parts.ts. The card never needed it.
`connection.expire` is deleted from the contract and from _EXPIRING_REQUESTS: the card is
raised with _emit, not _block, so nothing has emitted it since the operation lifecycle landed.
Sid's rule of record: a resolved card is fully dead; no path brings it back.
* fix(connectors): the watch loop settles once, on time, and never raises into the result
Three findings from the live review, one loop.
Continue racing a finished sign-in: the loop ran the gateway read, then settled. A read that
returned `connected` for an already-settled or failed target raised IllegalTransition out of
the tool and the model got a generic error instead of the per-app outcomes. The read now skips
targets that are not live (pending, initiated) and skips a settled operation; the loop checks
`settled` after every read.
Settle reason as row text: `settle()` wrote `continue`/`deadline` into each unresolved target's
`detail`, and the card printed it in red. The reason stays on the operation only.
Stop and the deadline waited for the next tick: `/stop` sets a per-thread flag with no wake
hook, so the sleep is sliced at 250 ms and the flag and clock are read each slice. The clock is
also checked before each read, not only after.
Tests: a failed mint that later reads connected settles cleanly; Continue during a read keeps
the settled result; no reason in detail; an interrupt settles within the same second.
* fix(connectors): MCP setup off the desktop returns unavailable instead of blocking
run_mcp_operation treated a non-None connection_callback as "a card exists". Every tui_gateway
session has that callback, the Ink TUI included, so an MCP install from the terminal UI blocked
until the 300 s deadline while the docs promised `unavailable` with the terminal commands.
The MCP path now reads the session surface the same way the managed path does; the callback is
never the predicate. Test binds the surface to `tui` with the callback attached.
* fix(connectors): a failed Try again shows the failure, not the old dead link
The panel's re-mint ignored the gateway's per-app status and moved the row to `initiated` with
whatever link came back, `None` included, so a mint that failed again rendered as waiting on the
link that had already died.
One reader of a mint response now serves both the first mint and Try again
(`managed.mint`, with the actor as a parameter). A repeated failure keeps the row `failed`,
drops the link, and carries the vendor's new text through `operation.refresh`, which emits a
frame without a state change so the card redraws.
* fix(connectors): a forced reconnect waits for the new sign-in before it reports connected
`reconnect` with `force: true` is the account switch. The vendor keeps the old account active
while the new link waits, so the first list read after the mint said `connected` and the
operation settled at once: the new link was dropped and the model was told the switch was done.
A forced target is marked awaiting_new_attempt after the mint. The watcher ignores its row until
the list shows the new attempt (`connectionStatus: initiated`) once, then trusts `connected`.
* fix(connectors): the operation registers under the gateway session key
The tool registered the operation under the agent's session_id; every RPC (connection.respond,
connectors.operation.status, the panel's connectors.connect) and the update emitter looked it up
by the gateway's session key. Those agree until compaction rotates the agent id mid-turn; then
the card's clicks find nothing, no update reaches it, and the tool waits out the deadline.
The registration key is now the bound HERMES_SESSION_KEY, with the agent id as the fallback for
callers with no gateway (unit tests, a bare CLI). The E2E passes a rotated agent id and drives
the card by the gateway key.
* fix(connectors): the forced-reconnect gate reads any non-active row; a failed re-mint of an expired row is failed
Three follow-ups from the verification of the fix pass.
The awaiting_new_attempt gate cleared only on the literal `connectionStatus: initiated`. The
field is optional on the wire and `initializing`, `failed`, `expired` are valid values, so a
forced reconnect could wait the full 300 s and swallow a failed new attempt. The gate now holds
only while the row still reads as the old account (`connected` or `active`) and releases on
anything else.
Try again on an `expired` row whose re-mint fails raised IllegalTransition (no expired → failed
edge). The re-mint steps through `initiated` as the user's attempt, then `failed`, then drops the
dead link.
`detail` never carries a state name any more: `failed` as detail rendered as the row label and
made agent/display.py tag the settled result as a tool error. Only vendor text goes there.
`connection.expire` removed from the renderer's unscoped-stream set; nothing emits it.
|
||
|
|
e0ef0eb9c3 |
manage_connections covers local MCP servers; setup_mcp leaves the schema (NS-867, PR1) (#109517)
* feat(connections): manage_connections covers local MCP servers; setup_mcp leaves the schema
One model tool now connects the user to apps of both kinds. A target
`{"name": "linear", "mcp": true}` is a locally configured MCP server;
`install` / `enable` / `authorize` are its verbs. Bare strings and
`{"name": ...}` stay managed connectors and that leg is unchanged.
MCP targets run through one backend-owned connection operation
(tools/connections_tool_operation.py): created with a server-side
deadline from the new config key `connections.wait_timeout_seconds`
(default 120, floor 5, no ceiling), per-target state, and exactly-once
settlement (all resolved / Continue / deadline / interrupt). Unresolved
targets freeze as `not_connected` with the settle reason.
Why the fold works now: the approval card is reached through
`agent.connection_callback` via the agent-level inline executor table,
which is the only path that carries a GUI callback. Registry dispatch
(every non-GUI surface) settles MCP targets as `unavailable` with the
`hermes mcp install / login` hint; managed targets in the same call
are unaffected.
`setup_mcp` is removed from every advertised toolset and from the
deferral list; an inline-table shim keeps calls from conversations
opened before this change dispatching (prompt-cache protection).
`_LEGACY_TOOL_ALIASES` is not the mechanism: inline tools bypass it.
Gateway: `mcp.setup.request/respond` are replaced by
`connection.request/respond/expire` (no wire compat; desktop ships
with this). The bridge waits exactly the operation's deadline. The
`session.resume` snapshot gains `pending_connection` so a reopened
window restores the card with the original deadline.
`manage_connections` joins `_SEQUENTIAL_DEADLINE_EXEMPT_TOOLS`: the
operation owns its wait; the 420s guard must not report `tool_timeout`
while the card is live.
The portal `check_fn` on the tool is dropped in favour of a
handler-level gate on the managed leg, so signed-out sessions can still
approve local MCPs.
* wip(desktop): connection.request store, resume restore, card routing for MCP targets
Renderer half of the setup_mcp fold, first slice: connection-request store
(mirrors clarify), connection.request/expire handling, pending_connection
resume restore, mcpTargets() + isCardTool(name, args) so MCP-target
manage_connections calls classify as cards. Not yet: the card component
rewrite (mcp-setup-tool.tsx), mcp-directory.ts removal, vitest, docs.
Does not typecheck until the card rewrite lands.
* fix(config): hermes update turns on the connections toolset for saved toolset lists
`hermes tools` writes an explicit `platform_toolsets.<platform>` list, and the
resolver reads absence from that list as "unchecked". The `connections`
toolset (#106842) shipped after most users last saved, so `manage_connections`
is stripped from the schema on every install that ever opened the picker.
The Nous entitlement gate never runs; the agent reports the tool as missing.
Migration 44 -> 45 (renumbered when folded into #109517; main was already at 44) appends `connections` to each explicit per-platform list
that lacks it and records the offer in `known_builtin_toolsets` where that
record exists, so a later uncheck reads as a decline. It skips: platforms
whose record already holds `connections` (the user saw the checkbox and left
it off), bare composite lists ([hermes-cli]) that already inherit it, platforms
where the toolset is not allowed, and any config whose `agent.disabled_toolsets`
names `connections` (Blank Slate, `hermes tools --disable`), because the
resolver subtracts that list last and the enable would never take effect.
The explicit-list test is the resolver's own: any configurable or plugin key.
`hermes update` runs migrations post-pull for the active profile and every
sibling, so one update is enough. Fresh installs and composite users were
never affected.
* refactor: anti-slop pass on the desktop slice; shorten added comments
Parse connection.request at the boundary with a typed wire interface instead of
unknown + typeof; mcpTargets reuses connectorText; comments cut to one or two
lines. slop-ratchet: no net-new findings in 13 touched files.
* feat(desktop): the MCP approval card answers manage_connections; MCP Directory removed
The existing card (mcp-setup-tool.tsx) now reads the connection-request store,
renders for manage_connections calls with mcp:true targets, answers through
connection.respond with a per-target outcome, and no longer calls reload.mcp
after Install; the new server's tools arrive on the between-turns refresh.
A settled operation renders the first target's frozen state.
session.resume restores a pending card with its original deadline on both the
activate and cold-resume paths.
lib/mcp-directory.ts is deleted along with its two fallback branches
(suggestion provider, card install). The catalog was already primary in both;
a catalog miss now yields no suggestion / a notInCatalog error. The GitHub
never-suggest test is rewritten on catalog-shaped data.
vitest: connection-request store (6), suggestion provider, clarify restore.
slop-ratchet: no net-new findings in 19 touched files.
* chore: drop __pycache__ files swept in by an over-broad git add
* fix(desktop): correlate the connection.request row with the model's tool call by reason
The synthetic row from connection.request and the tool.start row carried
different ids and no shared match value (op_id is not in the model's args),
so the card mounted twice. reason is the arg both sides carry.
* docs: manage_connections covers local MCP servers; connections.wait_timeout_seconds
* fix(connections): settle reason derives from target state, never from the renderer
A card that answers one of two targets and claims all_resolved must settle as
continue with the other target not_connected; found live with a two-target call.
* fix(desktop): a pending connection card re-arms on resume and activate
The store entry was restored but the transcript row was not, so navigating
away and back (or reloading) lost the card while the backend kept waiting.
restorePendingClarifyToolCall's core is generalized to any blocking tool
name and both resume paths project the connection row through it.
Verified live: card restored after navigate-away and after a full renderer
reload, deadline_at unchanged, approve settles connected.
* style: literal wording in added comments, docstrings and docs
* fix: shared gateway-event contract and config-schema category for the connection events
connection.request/expire replace mcp.setup.* in apps/shared gateway-events
(json list, BACKEND_EVENT_NAMES, GatewayEventMap) so the renderer's event
union includes them and the tui_gateway contract test passes. The new
`connections` config section folds into the agent tab like the other
single-field sections.
* style: import order (perfectionist) in the desktop and shared files this PR touches
* chore: retrigger CI (zero-job dispatch failure, auto-heal)
|
||
|
|
d3a44784b1 |
chore(desktop): backend contract v7 — blocking prompts are JSON-RPC server->client requests
A renderer built after
|
||
|
|
49c6d4a9e0 |
test(contracts): tests mirror tui_gateway/; the runtime-artifact spoof test asserts the new 4000
tests/contracts -> tests/tui_gateway/contracts (tree-layout rule: tests mirror a source package). test_rpc_params_cannot_spoof_runtime_artifacts: forged owner_transport / owner_session_record / owner_token keys are now refused at the wire (4000 + key path) instead of silently dropped before the handler; the invariant (no steer reaches the agent) is unchanged and asserted directly. |
||
|
|
24ffc8d23c |
fix(contracts): params validation rejects only unknown keys; accepted params + results are checked after the handler
Handlers own their documented domain codes (4006 missing session_id, 4015 bad
url, 4009 orphan claim); the contract's job on the way in is the one check no
handler performs — an unknown key (4000 with the key path). Missing/mistyped
fields are re-checked AFTER a successful handler answer under the strict
test policy, so a contract narrower than the wire still fails the suite.
Two models widened from the suite: SeedMessage (clients forward stored rows
verbatim), tool.complete.args (mirrored child rows omit it). Tests that
drove session.activate with prompt params (and vice versa) or stubbed
_live_session_payload with a bare {session_id} now send the real shapes.
|
||
|
|
d4840f9236 |
feat(contracts): Pydantic wire-contract registry, runtime validation and TS/OpenRPC generator (#110522, part 1)
tui_gateway/contracts/ is the single source of truth for the JSON-RPC wire: Params/Result/Payload bases, a registry of METHODS / SERVER_REQUESTS / EVENTS, runtime validation (unknown/mistyped params answer 4000 with the field path; a result or payload that violates its model raises under the test suite and logs once in production), and the server-request contracts + shared value shapes as the authoring template. scripts/gen_gateway_contracts.py renders the tables through a small JSON-Schema-subset walker into apps/shared/src/gateway-contract.generated.ts and gateway-contract.openrpc.json (unsupported constructs raise at generation). Method contracts for the 213 remaining handlers follow in the next commits. |
||
|
|
9f7f2f28c0 |
feat(gateway): server→client JSON-RPC requests replace the *.request/*.respond event pairs (#110521)
The gateway asked the user questions (approval, clarify, sudo, secret,
vault, MCP setup, the desktop read/act bridges) by emitting a
`<x>.request` EVENT carrying a hand-minted request_id, blocking the
agent thread on a module dict keyed by that id, and exposing a paired
`<x>.respond` METHOD per kind — thirteen pairs, four registries
(`_pending`, `_answers`, `_batch_clarify`, `_EXPIRING_REQUESTS`) and a
per-kind reconnect snapshot (`pending_clarify` / `pending_approval`)
that only two of the thirteen kinds ever got. JSON-RPC already has the
primitive: the server sends a request frame with an id and the client
answers with a response frame bearing the same id.
`tui_gateway/server_requests.py` owns the one mechanism:
send() block the agent thread until the response frame
(`srq-<n>` ids; ints belong to the client)
send_async() fire-and-callback variant (bot relay)
cancel*() withdraw with ONE `request.cancel {id, method, reason}`
event (timeout / interrupt / process exit /
answered elsewhere) instead of per-kind *.expire
open_requests() the still-open frames, replayed by session.resume,
session.activate and session.events.since so a
reconnecting client re-renders every kind, not two
clarify.lock stays a real client→server RPC (locks one batch
answer early); locked answers merge into the final
set even when the closing response carries only the
tail the user answered last
A client that does not implement a method answers -32601 and the agent
fails fast (the old fixed-timeout "unavailable" probes for tour/preview
still work — a wire error IS an answer). Approval: the queue entry's
settle hook withdraws the request when `/approve` from another surface,
a timeout or an interrupt resolves it first, so no window keeps a dead
card. Compute-host children own their waits; the parent mirrors their
open frames for replay and relays `clarify.lock` + response frames.
Clients: `JsonRpcRequestChannel` gains `onRequest` (unhandled → -32601,
dedup by id) and `JsonRpcGatewayClient` re-delivers `open_requests`
from the replay result. Desktop gets `gateway-event/server-requests.ts`
(one handler per method, replacing the request branches of
`input-requests.ts` / `desktop-bridge.ts`) and a `store/server-requests`
registry so every answer site calls `respondToServerRequest(id, result)`
synchronously; the TUI gets `createServerRequestHandler.ts` +
`serverRequestStore.ts`. `gateway-events.json` now pins both halves
(events + server request methods); the two contract tests check both.
Live (real stdio gateway, real `clarify_callback` on the agent thread):
before, `clarify.request` event + `clarify.respond` RPC, batch final
answers lost ('' returned); after, `{"id":"srq-…","method":"clarify"}`
frame, `session.events.since.open_requests` replays it, response frame
`{"answer":"yes"}` reaches the agent, batch lock + final response
merge to `{"q0":"1","q1":"free text"}`.
|
||
|
|
ebe8cda8ea |
feat(tui_gateway): real JSON-RPC server→client requests replace the *.request / *.respond notification pair
The backend never sent a JSON-RPC request; when it needed an answer from the
renderer it hand-correlated a `*.request` notification with a later `*.respond`
method through four module-level dicts, a timeout thread and 13 derived
`*.expire` names, plus a separate reconnect snapshot per prompt kind. That is a
second request/response layer built on a protocol that already has one.
`tui_gateway/server_requests.py` sends `{id: "srq-…", method, params}` and
blocks on the response frame with that id (string ids never collide with the
clients' integer ids). One `request.cancel {id, method, reason}` notification
withdraws a request on timeout / interrupt / session close. `open_requests` on
`session.resume` / `session.activate` / `session.events.since` re-delivers
unanswered requests after a reconnect; the shared TypeScript channel does that
itself before the caller sees the result. Batch clarify keeps its per-question
locks as a normal `clarify.lock` RPC (the last lock resolves the request).
Approvals stay queue-backed (`tools.approval` owns the timeout, `/approve all`,
coalescing): the request resolves the queue entry and the entry's own
resolution withdraws the request through `register_gateway_settle`.
Deleted: `_block`, `_respond`, `_pending`, `_answers`,
`_pending_prompt_payloads`, `_batch_clarify`, `_EXPIRING_REQUESTS`, the
`*.respond` methods, every `*.request` / `*.expire` event, `pending_clarify`.
Compute-host (turn isolation) mirrors the child's open request and relays the
response frame / lock to it. Desktop, TUI and shared clients register
`onRequest` handlers where they used to switch on `*.request` events; answers
are response frames over the socket the request arrived on, so #91684's
owner-routing class cannot recur for prompts.
|
||
|
|
31e0300b52 |
fix(tui_gateway): foreign-profile RPCs open state.db read-only
_profile_db acquired a registry WRITER on another profile's state.db for every RPC about it and closed it in the handler's finally — schema init plus the write lock on a store owned by that profile's own gateway, once per sidebar refresh under desktop app-global remote mode. Reads now go through the dashboard sidebar's read-only open helper; the four handlers that mutate (session.move_cwd, session.delete, session.set_hidden, session.foreign.import) opt in with writer=True. Field trace and read-only approach by @samalone on #109737. Co-authored-by: samalone <samalone@users.noreply.github.com> |
||
|
|
f72e79a111 |
fix(fallback): named custom providers keep their configured identity after automatic fallback (#98739)
resolve_runtime_provider returns the bare billing class 'custom' for every named providers:/custom_providers: entry; the configured id only survives in requested_provider. All three fallback resolvers (gateway, TUI/desktop, cron) persisted runtime['provider'] as the agent identity, so an automatic fallback labeled the session 'custom' in the UI and billing rows, while a manual /model switch to the same provider showed the configured name. New shared helper hermes_cli.fallback_config.effective_runtime_provider() upgrades the bare class back to the entry's configured identity (ad-hoc provider: custom entries stay unchanged), applied at all three sites — same class as the delegate_tool fix. |
||
|
|
0388d03f22 |
fix(tui_gateway): profile-scoped RPCs bind the requested profile's secret + terminal scope
_profile_scoped_rpc (mcp.servers.*, insights.get, cron.manage, skills.manage,
mcp.catalog, plugins.manage) bound only HERMES_HOME for params.profile.
config.yaml's ${VAR} expansion (config._env_ref_lookup) and the MCP probe's
header/env interpolation resolve through get_secret, which with no scope
installed reads plain os.environ, i.e. the launch profile's values. The
Desktop MCP setup Test-connection (mcp.servers.test) for a secondary thus
sent the default profile's token (or the literal placeholder) and reported
green against the wrong credential - the JSON-RPC twin of the dashboard
REST gap fixed in #110271 (#109901).
Bind the same home + secret + terminal composition a turn binds
(_session_profile_runtime_scope), hydrating the profile's external secret
sources first. os.environ is never mutated. Drops the now-unused
mcp_rpc_helpers.reset_profile.
|
||
|
|
105e27f962 |
fix(tui-gateway): unavailable profile param is JSON-RPC 4064, not a ws dispatch crash
_profile_home deliberately raises for an explicit target that no longer exists (an unavailable target must never fall back to the launch profile). Nothing translated that exception, so a desktop client still holding a deleted profile turned every profile-scoped RPC (session.create/resume, config.get/set, ...) into "ws dispatch crash" + -32603. Raise a typed ProfileUnavailableError (FileNotFoundError subclass, so method-level contracts are unchanged) and map it once in handle_request to code 4064 — the code the other profile-scoped surfaces already use for "profile not found". The display-only _response_profile_name fall-back from #107831 now catches the typed error. Folds the salvaged test into the existing target-unavailable file (one invariant test covering both methods and the display helper). Fixes #107829. |
||
|
|
8f6afbaf0d | fix(tui_gateway): handle deleted profiles gracefully in _response_profile_name (#107829) | ||
|
|
2770f93064 |
fix(profiles): reject traversal-shaped profile names in get_profile_dir
A WS 'profile' param like '../../foo' normalized to a path component that escaped the profiles root, letting a connected client bind an arbitrary existing directory as a profile home (state.db opened there, and session delete chains into per-id file cleanup under <dir>/sessions/). get_profile_dir now validates the canonical name against the profile id regex before joining it under profiles/. The regex only, not the reserved list, so pre-reserved-list dirs like profiles/hermes keep resolving. Callers that probe existence (profile_exists, _profile_home, the 4064 resolvers) treat ValueError as 'not found'. |
||
|
|
06bfcae47f |
fix(tui-gateway): launch-profile turns keep their env-only terminal policy once multiplexing is active
|
||
|
|
e5ca5207de | fix(agent): unify replay history canonicalization | ||
|
|
0b4df46255 |
fix(tui-gateway): stamp the login on the session record so rebuilds keep it
_session_auth_user_id read auth_identity off the transport slot at build time, but a second window replaces that slot with a FanoutTransport that has no identity, and branch and eager-resume builds run before the record exists, so those agents got user_id None. The record now carries auth_user_id from the creating transport, builds that run before the record pass it to _make_agent explicitly, and the compute host turn frame carries it too. A different login attaching to an existing session is logged once with both ids and the creator's id is kept. |
||
|
|
d532d83eca |
refactor(honcho): trim duplicated tests and long docstrings
Parametrize the sessionStart, injection-log, dashboard user_id, unresolved-peer and deferred-save tests that differed only in their inputs. Share the blocking remote in the concurrent flush tests. Fold _as_flag onto a word table. Cut the added docstrings to the what and the one non-obvious why. |
||
|
|
beab8b6f27 |
fix(honcho): keep observation flags across a flush rebuild, never orphan an evicted session, namespace dashboard logins
`_flush_session` discarded the observation flags when it rebuilt an evicted SDK session, and the cached path returned none, so recall fell back to the config snapshot. Both paths now return and store the flags. A deferred `save()` on a session the cap evicted puts it back in the cache, or flushes it inline when a newer object owns the key. `save()` and `stop_async_writer()` share the writer lock, and the writer drains its queue after the join, so a put that raced shutdown is written. The trim after a flush runs under the cache lock. The shutdown join takes the remaining budget instead of a fixed ten seconds. The injection audit file is created owner-only, and `logging: "false"` reads as off. The desktop passes `<provider>:<user id>` so a basic-auth alice and an OIDC alice are two peers. When a gateway platform supplies no user id, the peer notice and tool error no longer recommend peerName, which would merge every user of that gateway onto one peer. README documents `injection.sessionStart`, `logging`, and what a dashboard login does to peer resolution. |
||
|
|
c5973cd540 |
feat(tui-gateway): build the agent with the authenticated dashboard user as user_id
the dashboard login already stamps {user_id, provider} on the websocket
at upgrade time and tui_gateway keeps it as WSTransport.auth_identity,
but _make_agent never read it. every dashboard and desktop session was
built with user_id=None, so memory providers saw no runtime user and fell
back to the configured peer, mixing all logins together (#89794).
_make_agent now reads the session transport's auth_identity and passes
its user_id to AIAgent, the kwarg gateway platforms already use. the
legacy ?token= path, stdio, and the server-internal credential the PTY
child connects with carry no human and pass None, so they keep resolving
to the configured peer. the change is provider-neutral: honcho and any
other memory provider receive the id through the existing initialize()
kwargs.
|
||
|
|
991b23ad8d |
refactor(sessions): one session-id minter; QQ update-prompt key from build_session_key
Nine f-string sites minted `YYYYMMDD_HHMMSS_<hex>` independently with the hex width already drifted (6 on CLI/TUI/agent/import, 8 in the gateway store, 12 in portability imports). hermes_cli/session_lost_and_found.py classifies schema-less salvage rows by that shape, so a site drifting the prefix would silently change recovery. hermes_state_ids.new_session_id(now, hex_len=) is now the only writer and owns SESSION_ID_PATTERN; stdlib-only so agent/, cli.py and gateway/ can import it without the SessionDB graph. Widths are kept per site on purpose: the Desktop's session-id candidate regex is pinned to 6 hex chars for interactive ids; the gateway store and portability importer keep 8/12 (more rows per second). Not a bug, so not "fixed". gateway/platforms/qqbot/adapter.py hard-coded `agent:main:qqbot:<scene>:<chat>` for the update-prompt authz key, ignoring the profile namespace build_session_key applies; a secondary bot in a multiplexed gateway got `agent:<profile>:...` keys and its clicks were rejected. The key now comes from the one builder via BasePlatformAdapter._source_session_key. Behavior change: QQ update-prompt clicks are authorized under the profile-namespaced key (byte-identical `agent:main:` for the default profile). |
||
|
|
b91088d768 |
refactor(config): one effective-user-config loader replaces 9 hand-rolled raw→overlay→expand pipelines
Every defaults-free config reader (gateway runtime, TUI gateway, cron
scheduler + job snapshot, `hermes send` env bridge, doctor memory section,
hermes_cli/main early parse, hermes_time, hermes_logging, the gateway
fallback-chain refresh) re-implemented "read config.yaml + managed overlay +
${VAR} expansion" by hand, in three different orders, and none of them
replayed the model-key canonicalization or the last-known-good recovery that
load_config() gained. An admin-pinned `${VAR}` expanded on one surface and was
bridged literally on another; `model: {name: x}` resolved to an empty model
everywhere except the gateway.
hermes_cli/config_effective.py::load_user_config_effective is the one
primitive: user file → ${VAR} → managed overlay → _normalize_root_model_keys,
no DEFAULT_CONFIG merge, sharing read_raw_config's parse cache and serving the
last good parse (in-process, then backups/config/*.good.*) on torn YAML;
`fail_closed=True` raises for the one caller that keeps its own last-good
state (the fallback-chain refresh). gateway/run.py::_load_gateway_runtime_config
is deleted — it was _load_gateway_config plus expansion, and _load_gateway_config
now expands.
Behavior change: _load_bridge_config, send_cmd._load_hermes_env and
doctor_state._doctor_memory_config expanded BEFORE the overlay; they now match
load_config (managed `${VAR}` expands against the process env only). All nine
sites gain model-key canonicalization and last-good recovery.
send_cmd._load_hermes_env now routes its .env read through
env_loader._load_dotenv_with_fallback so the credential sanitizer runs.
|
||
|
|
9b9026e62f |
fix(kanban): drop the TUI empty-selection change and refit tests to the opt-in contract
Salvage follow-up to Xipong's #107736. Kept the core: `kanban` is a configurable, default-off toolset whose check_fn answers the schema build's own selection (ContextVar) instead of the legacy top-level `toolsets` key, so `platform_toolsets.<platform>: [.., kanban]` — what `hermes tools enable kanban --platform X` writes — actually reaches the gateway agent's tool schema. Dropped the `tui_gateway/server.py` change: turning an explicitly empty CLI selection from "all" into "nothing" is a separate behaviour flip already tracked by #107452, not part of this bug. The two TUI loader tests that asserted `kanban` is auto-recovered onto a saved `[memory]` list now assert the opposite: a configurable opt-in is never recovered. |
||
|
|
3d7f773bb4 | fix(kanban): honor explicit platform tool opt-ins across configuration surfaces | ||
|
|
fafb27ee5c |
feat(plugins): on_room_member_activity hook projects Group Chat member runtime events to plugins
A hosted room member runs on a hidden room_plumbing session with no client transport, so the tool.start/complete, approval.request, message.delta and reasoning.delta frames its turn already emits bottom out at stdio and vanish. Between turn.started and turn.settled in the durable room log a client sees a black box, and community clients (Hermes Crew) cannot render tool cards, approvals or live member status without inferring them from text. One seam in write_json (plus the connector bypass in tool_progress) re-routes those frames, stamped with the session's _hosted_room_task coordinates (room_id, thread_id, member_id, turn_id, task_id, execution_generation), to a new observer hook through the bounded per-consumer queues on_stream_* already use, so plugin code never runs on the token path. Nothing is written to the room log: deltas would exhaust a room's byte budget in minutes and checkpoint replay must stay a pure function of the durable events. The task stamp gains member_id (the driver already knows it; the proof did not carry it). Group Chat keeps execution, scheduling and persistence; plugins own presentation. |
||
|
|
606903badc |
fix(tui): bind launch-profile terminal scope once multiplexing is active
After any secondary profile home is served, launch-profile turns used to stay unscoped and fall back to ambient os.environ. Bind the launch home's own terminal policy in that case so a poisoned ambient bridge can never become the launch turn's authority (#107422 residual of #68559). (cherry picked from commit f81147c1e5d283837e5e27f4da79710da7025235) |
||
|
|
0dcadf6f41 |
revert: remove Collective Wisdom V1 (#94266)
Reverts the in-tree org skill-marketplace: hermes_wisdom package, three model tools, CLI/gateway/desktop/dashboard/Telegram/Slack surfaces. Later non-Wisdom work on shared files (guest onboarding i18n, dashboard startup schema, Slack adapter, tui_gateway) is kept; Wisdom-only call sites and config were stripped from those files. |
||
|
|
3c3ca061a6 |
fix(desktop): pin launch SessionDB handle to the launch home
The lazy _get_db() singleton followed get_hermes_home(), so a first touch inside the multiplex cron ticker's per-profile override window permanently bound the default backend to another profile's state.db (#102526). |
||
|
|
0e927c914d |
Guided first launch behind HERMES_GUEST_ONBOARDING: intro, guided chat, first task in default (NS-848, PR B1) (#107958)
* feat(desktop): port guided onboarding substrate Add seeded session creation, transcript directives, profile routing, and the shared window and pane primitives needed by the guided flow. Keep later-step mounts deferred and exclude provider selection and retry machinery. * refactor(desktop): anti-slop cleanup for substrate Assemble seed parameters in the existing create helper and use the owning transcript attribute type. Read the guaranteed gateway and connection contracts directly to remove runtime type probes and unchecked assertions. * test(desktop): create-overrides invariants Verify that reasoning and title overrides do not select a provider or model. Empty overrides and seeds add no parameters. * feat(desktop): port first-run cinematic window Play the cinematic behind the guest onboarding launch flag using bundled Collapse and JetBrains Mono. Give the native window its own controller and restore the app on skip, renderer deadman or native watchdog. Drop the perf scenario because it depends on the removed replay hook. Guided chat kickoff and app-shell gate wiring remain with their later steps. * refactor(desktop): anti-slop cleanup for cinematic Preserve audio and canvas behavior through named types and inferred results. Split the viewport node and frame drawing to keep control flow bounded. Cut comments that only repeat the code. * feat(desktop): add onboarding gate and answers stores Track cinematic, guided chat, handoff and completion in one phase record. Queue the guide after the intro and share pending kickoff work between callers. Keep existing saved answers while dropping retired preferences. Leave intro seen-state ownership with the cinematic store. * feat(desktop): port guided onboarding chat Add guided setup cards, runbooks, machine context, and onboarding presence. Connect transcript rendering and first-build progress to the desktop behind the onboarding flag. Leave session kickoff and handoff execution for the next step. * refactor(desktop): anti-slop cleanup for guided chat Keep directive and layout lookups typed. Remove unsafe test casts and isolate onboarding transcript calculations without changing the flow. * feat(desktop): connect guided onboarding to durable first-build handoff Start the guide only after its profile backend confirms bootstrap readiness. Seed or adopt the welcome chat, then transfer the first build to default with a durable receipt and explicit retry. Wire cinematic completion, screen stand-down, layout growth and progress check-ins. Save agreed preferences before creating the build and release prompt slots after storage refusal. * refactor(desktop): anti-slop cleanup for onboarding handoff Reuse the gateway request and error contracts. Isolate guide adoption and snapshot validation while preserving receipt recovery and reasoning overrides. Validate persisted receipt fields at the JSON boundary without coercion. Keep corrupt identities rejected and retain only the permitted test mocks. * fix(desktop): guided chat review fixes Wire the native machine probe so guided setup can suggest a name and offer the right first task. Restore the comments that explain the flow boundaries. The directive registration uses the launch flag to preserve ordinary chat. Ruling 6 folds active.ts into assembly to keep activity ownership together and removes the second greeting source so the seeded and visible greetings agree. * fix(desktop): handoff review fixes Probe the guide backend before switching profiles so a readiness refusal keeps classic onboarding on the current backend. Restore list-valued personalization coverage and routing rationale. Remove the obsolete setup status fixture. * chore(desktop): onboarding script cull and rehearsal recipe Document a temporary-state rehearsal using the existing onboarding flag and optional portal stand-in. Keep the main scripts unchanged and retain window growth for the guided chat. * fix(connectors): reject incomplete catalog responses * feat(gateway): scope connector controls to the owning session * feat(desktop): connect apps through native session-owned controls * feat(desktop): gate connector cards and enable free-tier access Use the launch flag before mounting connector controls so classic transcripts add no status requests. Allow existing free-tier identities through the read-only tool gateway gate and test the owning-profile RPC path with A’s launch gate. Keep authorization links out of previews. * style(desktop): format connector translations Apply Prettier to the connector copy blocks while preserving upstream translations and free-tier wording. * refactor(desktop): anti-slop cleanup for connector card Use the transcript JSON contract and concrete RPC parameters. Preserve malformed-value filtering at one string boundary and make the fixture and row types explicit. Keep connector execution and cancellation behavior unchanged. * feat(desktop): detect initial language from the OS Use the native machine locale when no supported language is saved. Preserve explicit choices and leave inferred languages out of config. * refactor(desktop): anti-slop cleanup for initial locale detection Keep unvalidated config values at the existing validation boundary. Pass no saved choice after that boundary has ruled it out, preserving locale precedence. * test(desktop): onboarding port test set Make native window tests reject duplicate IPC handlers and isolate disabled onboarding. Assert the active gate mock when onboarding re-enables. Keep the test set limited to behavior carried by the port. * fix(desktop): recover failed guide kickoff and reveal once The review found that a failed guide create stranded the solo shell and draft profile, and solo boot faded an already visible window a second time. Restore the prior route and layout, release onboarding through its existing phase record, and surface create failures. Let the film own the reveal while solo boot animates the visible resize. * fix(desktop): preserve transcript ownership across cards and handoff The review reproduced answers submitted to the focused chat, repeated questions disabled across sessions, handoff recovery using foreground identity, and mount-dependent progress history. Target each card’s own composer, scope settlement to its message and session, carry the issuing guide through handoff, and derive progress from its transcript with streaming activity. Reuse the existing owner ladder for exact and profile-only routes. * fix(gateway): preserve connector ownership with profile routing The review found that shared-primary profile metadata was rejected before connector dispatch, while desktop controls treated a missing registry id as missing ownership. Accept profile only as routing metadata and keep the live transport as authorization. Resolve card ownership through the existing exact/profile ladder, retaining ambient routing only for the single-backend case. * fix(desktop): resolve plugin roots and gate the Basic layout The review found that the first plugin build was seeded with a different installation’s fixed path, and the director ruled that flag-off layouts must match main. Resolve the running desktop’s plugin root before seeding a plugin build and register Basic only when onboarding is enabled. Keep the runbook wording and the ordinary four layout presets intact. * fix(desktop): clear review-fix slop findings The slop gate flagged an undocumented layout-data assertion and unknown-return types in the new test selectors. Record the layout registry invariant and preserve each selector’s return type. The only remaining production finding is the accepted connector-tools baseline. * fix(desktop): detect the OS language on a fresh install The review found that the merged English config default prevented the desktop from probing the OS language on a fresh install. Add an opt-in saved-values read so an absent choice remains distinct from saved English. Preserve default-valued English only for explicit language saves; unrelated settings saves must not turn a merged default into a language choice. Older backends ignore the new query options and keep returning merged English, preserving their existing desktop behavior. * test(desktop): make the flag-off layout registry test deterministic The flag-off test awaited the full controller import, pulling in the UI graph and installing application watchers just to read layout presets. That import took 9.5 seconds locally and timed out in the director's run. Move the existing trees and registration into a small layout-presets module. Production and the synchronous test use the same flag-gated registration, without starting the controller in the test. Keep the real registry invariant and dispose the test's contributions after completion. * fix(desktop): keep the transcript parser and ::ask behind the onboarding flag Register the guided chat's question card only with onboarding enabled. Restore main's whole-paragraph parser and contribution rendering when the flag is off, including its streaming prose behavior. Keep segmentation for the guided flow until B4 decides the parser's wider use. Restore main's two parser test files so its existing product and plugin contracts remain the flag-off check. * test: drop the onboarding and connector tests pending a later ticket Apply the director's ruling to remove B1's added test files and restore main's existing suites. Keep only the gateway route-reader mock contract that main's profile tests need against the shipped activation behavior; their cases and assertions stay intact. The flow's shape is not settled and B3/B4 rewrite it. The connector layer will also be reworked. The live CDP run is the flow check until a follow-up ticket brings tests back. --------- Co-authored-by: brooklyn! <brooklyn.bb.nicholson@gmail.com> |
||
|
|
a6ee31f55a |
feat(wisdom): add Hermes Collective Wisdom Agent V1 (#94266)
* feat(wisdom): add trusted publish and install foundation
* feat(wisdom): add private contribution loop
* feat(wisdom): add managed consumption workflows
* fix(wisdom): close cross-repository safety gaps
* fix(wisdom): align local package and lifecycle policy
* fix(wisdom): require explicit profile setup
* docs(wisdom): repin reconciled gateway head
* fix(wisdom): fence content downloads and approval receipts
* docs(wisdom): record generation-fenced downloads
* docs(wisdom): record unified delivery PR
* fix(ci): stop passing invalid classifier inputs
* docs(wisdom): remove internal requirements ledger
* feat(wisdom): localize dashboard and desktop copy
* feat(wisdom): complete local contribution and consumption UX
* style(wisdom): satisfy desktop lint
* chore(wisdom): refresh requirements pin
* test(dashboard): allow formatted profile copy
* test(wisdom): stabilize desktop interaction coverage
* fix(wisdom): surface dashboard action failures
* fix(wisdom): add repeatable Portal demo login
* feat(wisdom): add actionable skill notifications
* feat(wisdom): add notification install and update actions
* fix(wisdom): make Telegram skill alerts actionable
* fix(wisdom): always refresh demo Agent login
* feat(wisdom): embed Telegram notification actions
* fix(wisdom): preserve Telegram notifications after actions
* fix(wisdom): keep Telegram notification cards readable
* feat(wisdom): add Telegram candidate approval flow
* feat(wisdom): explain Telegram qualification reasons
* fix(wisdom): reconcile cross-surface candidate actions
* feat(telegram): add Collective Wisdom management command
* chore(wisdom): refresh Gateway contract pin
* chore(wisdom): advance Gateway contract pin
* feat(wisdom): align command UX across clients
* feat(slack): add Collective Wisdom management parity
* feat(wisdom): add security and professionalism reviews
* feat(wisdom): add first-time qualification guidance
* feat(wisdom): simplify qualification sharing choices
* feat(skills): add optional editorial metadata
* feat(wisdom): enrich legacy skill presentation
* fix(wisdom): harden review and update boundaries
* fix(wisdom): emit canonical review timestamps
* fix(wisdom): align with merged gateway and main
* wisdom: add agent-led sharing core (policy, evidence, schemas, templates, delivery, weekly job, share/install flows)
- hermes_wisdom/agent_led/: policy resolution (server > local > defaults),
7-day evidence builder that excludes bundled/hub/managed skills and
dismissed/handled/recently-suggested content hashes, strict pydantic
schemas for agent output with repair-or-reject, fixed copy templates
(Share / Teammate / Published / Update / Mute), idempotent retried
delivery ledger with stale-action resolution, weekly review job,
resumable Share and Install flows.
- prompts/: candidate review, recipient recommendation, share packaging.
- tests/wisdom/test_agent_led.py: 30 tests.
* wisdom: agent-led renderers and button action dispatcher
- render.py: Telegram HTML, Slack blocks, Desktop payload; editorial name
is the emphasized line, product label stays separate.
- actions.py: resolve opaque wa:<action>:<dedup> targets via the delivery
ledger; Not now -> dismissal, Mute -> fixed options, Share -> resumable
packaging flow, Install/Update -> plan command. Never publishes/installs.
* wisdom: CLI verbs, agent_led config default, conversational catalog skill
- hermes wisdom browse/review-week/act/share/dismiss/mute (all --json).
- wisdom.agent_led config block, default enabled.
- SKILL.md rewritten so natural-language catalog questions map to the CLI
verbs, share/install flows and fixed notification templates.
* wisdom: wire agent-led weekly review into gateway tick and Telegram buttons
- gateway housekeeping tick calls maybe_run_weekly_review with a home
channel sender when a Telegram adapter is available.
- Telegram: wa: callbacks resolved through the ledger (stale-safe), mute
duration keyboard, send_wisdom_agent_recommendation rich card + fallback.
* fix(wisdom): integrate local mediation and harden model and setup boundaries
* fix(wisdom): honor authoritative recommendation policy and defer on failure
* fix(wisdom): synchronize opaque suppression and recheck delivery preferences
* feat(wisdom): route weekly selection through the session-owned assessment queue
* fix(wisdom): prepare and submit the reviewed generated share package
* feat(wisdom): separate native Share preparation from publication consent
* feat(wisdom): sync native mute choices through a leased preference outbox
* feat(wisdom): bind native mute controls to durable preference choices
* feat(wisdom): add scoped desktop and dashboard notification settings
* fix(wisdom): revalidate feed recommendations before assessment and delivery
* fix(wisdom): persist validated delivery receipts before completing notices
* feat(wisdom): add private notification claim and receipt client
* Persist Wisdom send reservations and recover delivery acknowledgements
* Route legacy Wisdom controls through current native review
* Add typed private Wisdom operation outcome client
* fix(wisdom): make agent-led advice usable in the local demo
* fix(wisdom): keep requested consent outside proactive limits
* fix(wisdom): distinguish unavailable assessments and preserve digest text
* fix(wisdom): assess ongoing usefulness beyond the current task
* fix(wisdom): restore immediate qualification sharing controls
* fix(wisdom): separate qualification review from installation advice
* fix(wisdom): collapse review checklists and simplify sharing copy
* fix(wisdom): show compact sharing progress and publication receipts
* fix(wisdom): require credential prefixes rather than matching skill names
* fix(wisdom): finish package checks before presenting sharing consent
* fix(wisdom): scan local skills before qualification cards
* fix(wisdom): update moderation results on existing sharing cards
* fix(wisdom): keep sharing review accessible from receipt cards
* fix(wisdom): align mediated review cards and collapsible checks
* fix(wisdom): clarify clean security summary wording
* fix(wisdom): normalize consent plans and add explicit recheck
* fix(wisdom): keep install and update receipts concise
* fix(wisdom): collapse assessments and deduplicate operation cards
* fix(wisdom): restore private Portal review from native cards
* fix(wisdom): sync Portal publication to original consent card
* fix(wisdom): show local skill version on sharing cards
* fix(wisdom): skip agent recommendations for self-published versions
* fix(wisdom): simplify candidate notices and local-edit recovery copy
* feat(wisdom): submit locally reviewed packages with one confirmation
* feat(wisdom): expose safe receipt and outcome sync recovery
* wisdom: onboarding notice says detect and share, names the user's own skill
Copy review from the product owner on the first and returning
qualification notices (fixed delivery mode):
- the feature blurb now says the org enabled detection *and sharing*
- both notices say the detected skill is one the user created
- both close with an exclamation mark
Applied identically to hermes_wisdom.notice, the desktop and web i18n
strings, and the tests that assert the sentences.
* wisdom: one opener, no approval line, ask to share after the skill is shown
Product owner review of the candidate card.
- The Hermes written card now opens with the same sentence as the fixed card
("Your organisation has enabled Collective Wisdom, a feature designed to
automatically detect and share useful skills across all team members.")
instead of its own blurb, so there is one first time message.
- "Nothing is shared without your approval." removed from Telegram, Slack
and Desktop. The buttons already make the permission explicit.
- "Would you like to share?" no longer appears before the skill is named.
It is now the last line, after the skill name, description, why suggested
and the checks, and reads "Would you like to share it?" (matching the
agent led template wording).
Tests updated for the new order; proposalNotice removed from all desktop locales.
* wisdom: American spelling, organization
Product owner decision: user facing copy uses American spelling.
Changes "Your organisation" to "Your organization" in the chat notice,
the Hermes written card opener, the desktop and web strings, and the
tests that assert them. Identifiers such as nas_organisation:* and the
German and French locales are untouched.
* wisdom: candidate card copy round 4 (owner review)
Apply the product owner's round 4 copy decisions to the Hermes Collective
Wisdom candidate card on Telegram, Slack, Desktop and the shared views:
1. Hermes-written cards are titled "Hermes Collective Wisdom" instead of
the bare "Collective Wisdom".
2. The "Reusable skill ready to review" line is gone from the candidate
card (Telegram rich card and plain fallback, legacy agent-led share
template).
3. The skill name and description are labelled: "Skill name: <name>" and
"What it does: <description>" (Telegram, Slack, Desktop).
4. "Why suggested:" is now "Why others might benefit:".
5. A passing professionalism review reads "Safe to share at work ✓ (no
inappropriate content found)" with no per-check bullets and no "Pass";
a failed review reads "Needs a look before sharing at work (possible
inappropriate content)" and lists only the checks that flagged
something. Pending/unavailable wording is unchanged.
6. Telegram button toasts: "Will ask later...", "Preparing more
details...", "Sharing...".
7. Qualification reasons: "You used this skill consistently across many
days." and "You've really refined this skill."
8. prompts/wisdom_candidate_review.md asks for a compelling
editorial_name, a simple one_line_description and a compelling
why_coworkers_benefit under 300 characters; "Be concise and
convincing." becomes "Be concise and compelling: the goal is that the
user wants to share it."
Tests updated for the new strings; review_text() gains direct coverage.
* wisdom: re-apply owner copy after rebase
- Native share cards (advice_view/interaction_view): drop the approval line, ask "Would you like to share it?" as the last line after the checks
- Hermes-written completion card titled "Hermes Collective Wisdom"
- Qualification reasons use the owner wording (consistently across many days / really refined)
- American spelling (organization) in remaining English copy
- Desktop test asserts the current Share button; web test matches the returning notice
* fix(wisdom): pin reconciled Gateway and verify Unicode hash vectors
Pin Gateway 60cd2d6b613ae3cd4a6e65155d1142006d907e78 and byte-identical producer artifacts. Verify every content-order case and package-manifest binding. Validation: 186 focused Python tests, Ruff and contract verifier.
* fix(wisdom): reconcile optional SDK tests and frontend lint
* fix(wisdom): default to agent-written notification summaries
* fix(wisdom): restore deferred install review and browse controls
* feat(wisdom): inspect installed setup with exact package provenance
* feat(wisdom): run native-approved installed setup steps with durable evidence
* fix(wisdom): recover interrupted setup with explicit native consent
* feat(wisdom): hand native installs into guided setup review
* fix(wisdom): continue requested setup with fixed notification copy
* fix(wisdom): preserve setup while waiting for a session model
* fix(wisdom): expose canonical setup review controls on desktop
* fix(wisdom): resume setup after recorded automatic updates
* fix(wisdom): make missing setup prerequisites recheckable
* chore(wisdom): align Agent with verified Gateway contract
* fix(wisdom): stop guessing team slugs in portal links
* fix(wisdom): retire pending advice on account sign-out
* fix(wisdom): cancel advice after terminal account revocation
* fix(wisdom): fence feed responses across account sign-out
* fix(wisdom): checkpoint signed-out feed before reactivation
* fix(wisdom): link proactive advice to scoped notification settings
* fix(wisdom): coalesce queued publication recommendations by version
* fix(wisdom): keep package review navigation local and deferable
* fix(wisdom): reflect installed state in discovery controls
* fix(wisdom): show exact checks before command confirmation
* chore(wisdom): pin bounded analytics privacy contract
* chore(wisdom): pin retired legacy notification contract
* feat(wisdom): review publisher usage with exact sharing copy
* fix(wisdom): align discovery and review check summaries
* fix(wisdom): show expired consent before confirmation
* fix(wisdom): require fresh review for legacy install controls
* fix(wisdom): preserve review expiry across check toggles
* fix(wisdom): retain update policy in native install reviews
* fix(wisdom): surface failed native card edits
* fix(wisdom): persist local command approval reviews
* fix(wisdom): use saved approvals for messaging commands
* test(wisdom): provide scan result in setup handoff fixture
* test(wisdom): exercise Telegram approvals with saved review state
* fix(wisdom): retain suppression policy for offline deferral
* fix(wisdom): reconsider candidates after deferred suppression expires
* fix(wisdom): bind review checks and report verified readiness separately
* fix(wisdom): persist accepted publication intent and recover exact outcomes
* fix(sync): pin UTF-8 tree ordering across writers
* chore(wisdom): pin organisation-scoped Gateway authorization
* fix(wisdom): restrict consent delivery to user-facing sessions
* chore(wisdom): refresh reviewed Gateway contract pin
* fix(wisdom): preserve kept tools in Blank Slate exclusions
* test(auth): reset anonymous fixture with a profile-scoped cache
* fix(wisdom): gate local surfaces and work on current profile entitlement
* fix(wisdom): invalidate quiet tool cache on entitlement changes
* test(wisdom): authorize local consent gateway fixtures
* fix(wisdom): keep entitlement decoding free of native crypto imports
* test(wisdom): provide local entitlement to demo CLI subprocess
* ci: leave upstream workflow unchanged in Wisdom PR
* fix(wisdom): ship package and contracts in Nix wheels
---------
Co-authored-by: hbizi <36184542+hbizi@users.noreply.github.com>
|
||
|
|
28388947f8 |
fix(desktop): preserve local provider identity across new chats
Report a routable provider in session.info instead of the resolved custom billing class. The desktop carries that identity into new chats without an endpoint, so losing llamacpp could send a local model to a cloud API. Recover managed identity from the ownership-checked endpoint through the existing custom-provider lookup. Bind metadata recovery to the session's profile, preserving named endpoint precedence, pending selections and remote compute metadata. Cover new-chat and resume routing under a cloud default, conflicting profile mappings, and negative endpoint-ownership cases. |
||
|
|
3b01b4ce0f |
feat(desktop): Nous free tier on Hermes Desktop (#105260)
* feat(desktop): free-tier state over RPC, status routes that name it, and a sign-in that keeps connectors The desktop learns about the Nous free tier by reading local auth state (pull): free_tier.status answers has_guest / enabled / carries_inference / notice_pending with zero network, and free_tier.ack_notice persists the one-time notice flag on the identity itself. setup.runtime_check reports free_tier for the selected route; /api/portal, the Nous card in /api/providers/oauth and billing.state carry free_tier (billing answers the free tier locally instead of a portal call that can only fail). The free-tier picker row carries an explicit free_tier_row flag and is never priced or locked. POST /api/providers/oauth/nous/start over a free-tier identity registers the connector transfer and returns its code and consent URL; the poller waits for the transfer before the token grant, persists the account, runs settle_after_upgrade, and the poll response gains reason, account_email and model. * feat(desktop): free tier on Hermes Desktop: ready screen, notice strip, status chip, Billing view, one sign-in dialog The renderer reads the free tier from free_tier.status (pull) into one store; the first-launch intro is the same state rendered two ways, keyed on the backend's one-time flag: the onboarding overlay opens on a ready screen when the free tier carries inference, else a one-time strip above the composer. Settings > Billing gains a free_tier view (notice with one Sign in, Plan / Model / Connectors summary, plan card, footnote; no payment or usage rows). A status-bar chip names the tier and model while it carries inference. Every entry point opens one claimed sign-in dialog that drives the extended oauth/nous route and maps the poll's status and reason to the ruled screens; Done settles billing, model options, providers and re-homes a session still on nous/welcome. The picker badge also fires on free_tier_row. Docs: Desktop section in the free-tier guide, AGENTS notes. * fix(desktop): free_tier.status starts the free tier's background setup when no identity exists A served backend has no session-setup moment like the CLI's, so beside an explicit provider the free tier was never set up on the desktop: no connectors, no notice strip. The first status read now starts the same one-attempt background setup; the call itself never waits. * fix(desktop): one Sign in on the Billing page; Settings > Providers names the free tier, never Connected The free-tier plan card is the what-you-get text alone (the notice carries the page's one Sign in). The Nous provider row reads Nous · free tier with a Free tier tag while the identity is the free tier, instead of Nous Portal · Connected. * fix(desktop): Settings > Providers never files the free tier under Connected * fix(desktop): the intro's shape is keyed on the route, not on the identity free_tier.status reports available (an identity exists and the tier is on); whether inference runs on the free tier is setup.runtime_check.free_tier, keyed on the resolved endpoint. The ready screen shows when that route is the free tier; the composer strip when the user's own provider carries inference. An own-key install used to get the ready screen. * docs(desktop): say what the free-tier chip is keyed on * fix(desktop): the featured Nous row's pitch on the free tier says what signing in adds * fix(desktop): a cancelled or superseded sign-in attempt can no longer change the identity or hide the intro Four lifecycle holes from review. The Nous poller checks the session's cancelled flag after the transfer wait, after the token grant, and once more under the session lock together with the save, so a sign-in the user abandoned never persists. The renderer's sign-in store carries an attempt generation that every continuation checks after each await, so a poll from a closed attempt cannot publish over the one on screen (and its backend session is cancelled). The ready screen comes down only after the backend recorded the acknowledgement. A composer still mounted takes over the notice claim when its owner unmounts. One thin test per hole. |
||
|
|
d9ca9c974d |
feat(vault): two-factor codes — automatic from a saved authenticator key, otherwise asked for in the user's UI
Follow-up to #106480. Sites that ask for a code after the password stopped the agent cold: the login classifier excludes one-time-code fields on purpose (a password must never land in an OTP box) and there was no tool for the second step, so the only move was to ask in chat. browser_vault_enter_code Fills the one-time code the current page asks for. Two sources, same invariant as passwords (the code goes to the page over the supervisor socket and never enters model context): - a TOTP seed on the login: local vault `otp_secret` (RFC 6238, stdlib, verified against the RFC test vectors), 1Password `op item get --otp`, Bitwarden `bw get totp`. Nobody is asked. - no seed: the surface prompts "Verification code for {site}"; the user types what their phone/email/app shows. Enter on empty / Skip declines and the tool returns code_declined ("do not ask again this turn"). no_code_field tells the model the site wants a passkey / hardware key / app approval: hand it to the user's device and wait for navigation. Per-digit OTP boxes (maxlength=1 pattern) get one digit each in DOM order. Surfaces CLI: sudo-style panel, code shown as typed (not a secret worth masking, typos must be visible), Enter submits, ESC/empty skips. Desktop: "Verification code for {site}" card via vault.code.request / vault.code.respond (gateway), owner-routed like the other vault prompts. Settings → Passwords & Logins: optional "Authenticator key" field on the add form (base32 or otpauth:// link); items with one show a "2FA auto" badge. `hermes vault add` asks for the same optional key. browser_vault_fill's result now says what to do next ("if the site asks for a verification code, call browser_vault_enter_code with this handle"). Six locales. Verified live (real model, local 2FA site that checks the TOTP; CLI PTY): A. login saved with authenticator key → signed in through 2FA, zero prompts, code/password absent from the transcript B. login without key → code panel → user types code → signed in C. panel dismissed → agent stops and explains, never asks in chat Unit: RFC 6238 vectors, seed normalisation, mint-without-asking, per-digit spread, decline, no-code-field; Desktop card test (owner routing, trim, Skip). |
||
|
|
d5aaaa4a1b |
fix(tui-gateway): a hidden seed row stays out of search, a partial seed copy is rolled back, live resume counts the wire (#107562)
Two independent reviews of the seeded-create change found three more places where the newly durable hidden row, or the new create-time copy, was not handled by the same rule as the rest of the path: - Message search (dashboard search and the session_search tool) had no display_kind filter, so a hidden opening row matched a query the person never saw. The shared search predicate now skips hidden rows. - _seed_row left the fresh session row behind when the transcript copy failed after the row was committed. The first prompt's retry copies the whole seed, so a kept partial copy would be duplicated. The row is now deleted when the copy did not complete, the compensation _persist_branch applies to branch children; the first prompt then starts clean. - _live_session_payload (a resume that reuses a live session) reported message_count as the raw history length while its messages array was filtered. It now follows _resume_response: the stored size when messages are omitted, else the wire count. Tests: the two seeded-create tests now drive the first-submit path through _persist_session_row_for_submit, the function prompt.submit calls, and assert search and the reuse-live count; a third test pins the rollback (no row after a failed copy, one copy after the retry). |
||
|
|
98d11c95f4 |
feat(vault): zero-setup UX — save a login on the page that needs it, managers auto-detected, one "Passwords & Logins" surface
Nobody should have to learn `hermes vault add` or find a toggle before "log into GitHub" works. - browser_vault_save_login: when the agent reaches a sign-in page with no saved login it asks the user on THEIR surface (CLI two-step panel on the sudo modal: identifier shown, password masked; Desktop card with labelled Email/username + Password fields). The answer goes to the encrypted vault bound to the page origin and is filled at once; the model gets back only the handle and identifier. Declining returns save_declined; headless sessions get prompt_unavailable. Never a password in chat. - Vault tools ride with the browser toolset (check_browser_requirements) instead of appearing only once the vault has items — an empty vault is exactly when save_login is needed. browser_vault_list hints at it when empty. - 1Password / Bitwarden are login sources as soon as their CLI is installed; `vault.<name>.enabled` is opt-OUT only. Settings shows Detected/Locked/Unlocked/Off/Not detected with a switch only for installed managers; `hermes vault sources` reports detection, `--disable`/`--enable` flip the opt-out. - Desktop nav/page renamed "Passwords & Logins"; empty state tells the user they do not need to add anything; all five locales updated. Docs rewritten from "how it works" to "say log into X". - New per-thread SaveLoginPrompt callback (agent/vault_backends/unlock.py) installed beside the unlock prompt on every CLI site and the gateway bridge (vault.save_login.request/respond/expire), propagated to worker threads via tools.thread_context. Live: CLI PTY (real model, packaged Chromium, local login server) — panel shown, identifier + masked password typed, server received the correct password, password absent from terminal transcript and from every file under HERMES_HOME outside vault/. Native Electron (headless, isolated HOME/HERMES_HOME, own Vite + CDP port) — card shown, "Save & sign in", server received the password, Settings lists the saved item, password absent from the rendered UI. |
||
|
|
8e7e9be217 |
feat(vault): sign in with 1Password or Bitwarden logins, unlocked per session
The browser vault now draws from three login sources behind one handle
shape: the local encrypted vault (vault_…), 1Password Login items (op:…)
and Bitwarden Password Manager logins (bw:…). browser_vault_list aggregates
metadata across them; browser_vault_fill routes by prefix and resolves the
password at fill time only, through the manager CLI.
External managers are locked until the user unlocks them for the current
session. The new browser_vault_unlock tool (and the fill path, implicitly)
asks the surface to show a masked master-password prompt — CLI panel
(reuses the sudo panel state), TUI/Desktop via a vault.unlock.request
blocking card. The password goes to `op signin --raw` / `bw unlock --raw`
on stdin, never argv or env; only the session token is kept, in memory,
with a 30-minute idle TTL, cleared on session close or `vault.lock`.
Headless contexts (cron, webhook, api_server, -q) can never prompt: the
manager is reported as locked with unlock=unavailable_in_this_session and
fill refuses — the same posture approvals take where nobody can answer.
Config: vault.onepassword / vault.bitwarden {enabled, binary_path, …};
a 1Password service-account token skips the prompt for headless use.
RPC: vault.sources, vault.source.set, vault.unlock, vault.lock for Settings.
Tests (2, real subprocess against a fake bw; each proven red by sabotage):
headless never prompts or spawns; unlock feeds stdin only, token never
enters os.environ, fill routes by prefix and the password only reaches the
fill script.
|
||
|
|
cde0ad0edd |
feat: agent signs into sites from an encrypted local vault (CLI, browser fill, Desktop Settings)
Consolidated re-apply of #96988 onto current main. Ported from Merit-Systems/OpenInstinct (MIT) opaque-handle autofill design: the model sees vault handles + login metadata, the password is resolved and filled server-side over the supervised CDP socket, and filled values are scrubbed from every browser tool result by an unconditional redaction registry. Rebase adaptations to the Sep-2026 facade/sibling layout: - toolsets: one _HERMES_CORE_TOOLS entry (the browser toolset derives from it) - hermes_cli/main.py: vault parser registered via the subcommand owner table - file_safety: vault/ joins the _READ_DENIED_DIRS credential-dir table - redact: registry scrub runs before the redact_secrets early-return - browser_vault_tool: _run_browser_command now lives in browser_tool_session |
||
|
|
a39fedff34 |
fix(tui): a real named profile "hermes" is not swallowed by the legacy-basename alias
"hermes" matches the profile-id regex, so canonicalising it unconditionally at the RPC boundary misrouted a genuine <root>/profiles/hermes to the default profile. Alias only when no such named profile exists; ".hermes" can never be a real id and stays aliased. Also: profile_name_for_home collapses its duplicated pre/post-resolve block into one loop over (path, resolved path) and drops the bare "parent named profiles" fallback that bypassed named_profile_home's root check; _profile_home goes back to main's single resolve() comparison; the symlink-loop assertion in the target-unavailable test is no longer wrapped in a try/except that could silently skip it. |
||
|
|
e1b0ee6f03 | fix(tui): fail closed on unavailable profile targets matching custom root basenames | ||
|
|
298893df66 | fix(tui): resolve default profile session names | ||
|
|
8b01df963d |
feat: expose session-scoped subagent roster and live tail RPCs
Project live children and background units for shared TUI/Desktop consumers; read a bounded tail from the existing runtime transcript. Reuse existing queued steering semantics rather than claiming delivery. Projection adapted from PR #70899 with exact live session and transport ownership. Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com> |
||
|
|
b3014688f9 | fix: isolate shared session membership and prune departed viewers | ||
|
|
68ed3ffd10 |
fix(tui_gateway): check steer authority by transport membership under fan-out
subagent.steer resolves authority by comparing the request's context-bound transport with the session's transport slot. Once a session mirrors to more than one client that slot holds a FanoutTransport, so the comparison fails for every client, the peer that commissioned the subagent included, and every steer is rejected. Fan-out without this check ships that regression, and no existing test catches it because the suite only exercises single-client sessions. Authority now asks whether the request's transport is attached to the session, directly or through the fan-out. The single-client case is unchanged: a bare slot still compares by identity. This widens authority. Any client attached to a mirrored session can steer that session's subagents, not only the peer that commissioned them. Narrowing it back to the commissioning peer requires recording that peer per subagent, which this change does not do. tests/tui_gateway/test_multi_client_fanout.py pins the commissioning peer's authority inside a fan-out, pins the widened case, and keeps a single-client control in which an unattached client is still refused. The four browser.controller.* handlers gate on the session transport slot exactly as subagent.steer did, through one shared gate in the _controller_method decorator, and the conversion missed them. Once a session mirrors to more than one client the slot holds a FanoutTransport, which is identical to no peer's WSTransport, so browser.controller.register, .result, .heartbeat and .detach all answer "session is not owned by this transport" for every client, the peer that registered the controller included. Browser control is therefore unusable on any mirrored session. No existing test catches it because the browser-control suite only exercises single-client sessions. All four now ask the same question steer asks: is the request's transport attached to this session, directly or through the fan-out. The single-client case is unchanged, because a bare slot still compares by identity. Only registration widens. On .result, .heartbeat and .detach the broker's is_owner check sits below the session gate and compares controller.owner is owner against the transport recorded at attach time (gateway/browser_control_broker.py), so a mirrored peer that did not register the controller is still refused there, now with "controller is not owned by this transport" instead of the session message. browser.controller.register has no such check, so any client attached to a mirrored session may register a controller for it; the broker's principal lane keeps that inside one authenticated identity, and a second identity in the same lane hard-replaces the first. tests/tui_gateway/test_multi_client_fanout.py pins the registering peer's access inside a fan-out, pins the widened and broker-refused cases, and keeps a single-client control in which an unattached client is still refused on all three scope-gated handlers. |
||
|
|
de25545dce |
fix(tui_gateway): fan session events out instead of rebinding the transport slot
A session held exactly one transport, and prompt.submit, session.resume, session.activate, and the queued-prompt drain all rebound that slot. A second client therefore took the stream away from the first: the earlier client stopped receiving the turn it was already rendering, and either client disconnecting parked the whole session on the drop sentinel. FanoutTransport goes in the same slot and satisfies the same Transport protocol, so write_json and every other reader of the slot are unchanged. It delivers each frame to a snapshot of its peers, concurrently when more than one peer is attached and the caller is not on an event loop, and prunes any peer that returns False or raises. A dead client is dropped; a slow one costs the emitter at most one write timeout per frame rather than one per peer. Request/response RPCs are unaffected: they still answer on the request's context-bound transport, so a client only ever sees replies to its own calls. The rebind sites become attach sites through _attach_session_transport, whose ladder keeps the single-client shape identical. The same object already in the slot is a no-op; an empty, stdio, or parked slot is taken outright; only the arrival of a second live client wraps both. The queued-prompt drain is included because it pinned the drained turn to the queuer and silenced everyone else. A non-peer newcomer such as stdio or the drop sentinel never displaces a live client, so an activate dispatched without a bound websocket cannot silence the socket that owns the session. Disconnect detaches first. A session that retains another client keeps streaming and is neither parked nor reaped, and only the clientless ones follow the existing close_on_disconnect and park-sentinel path, so a single-client disconnect, the orphan reaper, and its grace window behave as before. _ws_session_is_orphaned is unchanged: it still asks whether the drop sentinel is in the slot, and a fan-out is never the sentinel, so a session that still has a peer is never reported as orphaned. Attach performs no entitlement check: any authenticated peer may mirror any session. The fan-out architecture follows the approach in #40822 by @OmarB97. What the slot's later history forces. _close_sessions_for_transport drops the #83716 rebind-to-the-most-recent-surviving-viewer, which fan-out membership subsumes — a pop-out window is a peer, so a session that still shows in one is never returned as clientless — and keeps the #77129 revalidation before parking, now expressed as a liveness check under _session_transport_lock so it is race-free against attach and detach. _transport_is_live_peer defers its last answer to _transport_is_dead: a socket that already latched _closed is a departed client, and admitting it would keep a session out of both the park and the reap. _transport_is_dead also learns the fan-out: a FanoutTransport with no live peer is dead, so a session whose peers were all pruned by failed writes cannot outlive the TTL and LRU reapers. The upstream test that pinned the #83716 rebind, test_close_transport_rebinds_session_to_remaining_viewer, is re-expressed in fan-out terms: both windows attached, the pop-out closes, the session stays with the main window unparked and still receiving frames. |
||
|
|
04767e7aaa | fix(display): distinguish estimated context from provider usage | ||
|
|
026e3e84ea | fix: reject unavailable desktop profile and session targets | ||
|
|
d87d7657c0 | test(desktop): require successful isolated hook turns and exact calls | ||
|
|
3094cde513 |
fix(desktop): honor configured shell hooks for each chat profile
Wire the existing consent-aware, profile-keyed hook registrar at agent construction, where the correct session home is already bound. This covers serve and TUI agent construction without a startup-only registration or a new helper that swallows registration failures. Live isolated serve/WebSocket probes reproduce the missing registration on base for both write_file and terminal, then verify each consented profile blocks its configured tool while the unapproved profile still runs normally. Repeated alpha construction does not duplicate hook callbacks. Slim implementation of the agent-build placement proposed in #57020; thanks also to the profile-scoped analysis in #102691. Co-authored-by: grimmjoww578 <willies578@gmail.com> |
||
|
|
ebf2473325 |
refactor: share CLI goal commands across interactive surfaces
Keep parsing, contracts, gates and persisted goal mutations in one dispatcher. Adapters retain authorization, rendering and scheduling; TUI drafting resolves the target session profile off the RPC reader. Document ACP as unsupported rather than implying a goal loop exists. |
||
|
|
b0f87d7a7a |
fix(tui): publish session.control.update on every control mutation path; drop the Desktop-only heartbeat driver
- /goal and /loop are command.dispatch built-ins (never reach the slash worker), so the worker-branch publish never fired for the most common typed commands and the structured card stayed stale until the next turn. The publish helper now lives beside _snapshot_control in methods_session_control.py and is called from both slash paths plus the end of the turn tail — after the goal judge / loop tick evaluation, which mutate state AFTER message.complete. - session.control skips the duplicate emit for dispatcher-backed actions (still exactly one update per mutation). - Removed tui_gateway/desktop_heartbeat_driver.py, the entry/ws hooks and the hermes_cli lock rework: #104224 drives /heartbeat from the existing per-session poller for every TUI client. - test_session_control.py: dropped presentation-shaped cases, added the publication invariants (both slash paths, unknown dispatch publishes nothing, real _run_prompt_submit turn publishes post-judge). 3/3 new tests fail on the contributor backend. |