Commit Graph

24 Commits

Author SHA1 Message Date
Teknium d3202bbc8d feat(tui_gateway): fire agent_loop_stopped on session.interrupt too
Widens the new hook to the sibling interrupt surface: the TUI/desktop
session.interrupt path stops a live turn exactly like the gateway's
/stop, so plugins holding per-turn external resources get the same
signal there (platform='tui'). Gated on a genuinely running turn;
dispatch failures are swallowed so a plugin can never break the
interrupt. Docs updated to describe both surfaces.

Inspired by ChatGPT Work / Codex CLI 0.150.0 'Interrupt' hooks
(hooks that run when an active top-level turn is interrupted).
2026-09-12 22:19:44 -07:00
Teknium e7136f1694 fix(tui_gateway): profile sessions never read or write through the launch store
Three wrong-store paths in the TUI/Desktop backend under multiplexing:

- insights.get was scoped=True and then called _get_db(): before the launch
  handle was pinned (#102526, #108074) a scoped first touch bound the
  process-wide handle to the requested profile's state.db; even pinned, the
  RPC answered for the launch profile whatever `profile` said. Route it
  through _profile_db so it counts the requested profile's sessions.

- prompt.background side agents were handed the launch handle, so a
  named-profile Bot Chat's bg_* rows appeared in the default profile's
  session list and were missing from the profile's own history. Inherit the
  parent agent's dedicated _session_db.

- session_lifecycle's gateway-owned-source guard and the notification
  poller's compression-tip resolver looked the session up in the launch
  store, where a named-profile row does not exist: the guard was dead and
  a compression-rotated profile session lost every post-compression
  delegation/background completion (the fail-closed owner gate never
  matched the compressed parent's key). Both now use the session's own
  store via _session_db(session).

Addresses #102157, #102526.
2026-09-11 15:26:09 -07:00
Teknium dedc99ec6c fix(vault): ownership and race findings from the second independent review
Manager tokens: lock generation fence (a Lock acknowledged while `bw unlock`
/ `op signin` is still running discards the late token); tokens record the
unlocking gateway session and are released when THAT session ends, not when
any sibling session in the profile is torn down.

1Password: OP_CONNECT_HOST/TOKEN come from the profile's scoped secret store
like the service token (Connect outranks a service token inside op), never
from the launch environment.

Vault RPCs bind params.profile (home + secret scope) so a shared remote
backend serving several profiles locks/lists/unlocks the requested one;
unknown profile → RPC error, not a crash.

Fill target: inspection stamps are `<nonce>:<index>`; a fill resolves only
its own inspection's stamps, so an interleaved second inspection can no
longer redirect A's password into a newly mounted field (real Chrome: 0
filled, both fields empty).

Desktop Settings: every RPC goes through the owner profile's socket
(requestGatewayForProfile), query keys carry (connection, profile), an owner
change closes dialogs and wipes drafts (a master password typed for A is
never submitted to B; a late list from A never paints under B), and vault.add
secrets travel in a ref consumed by the mutationFn instead of mutation
variables. Three owner-routing invariant tests on the real component.

Docs/PR body: session-scoped release, lock-race semantics, bw --passwordenv.
2026-09-10 10:35:07 -07:00
Teknium 5aa9e7f033 fix(vault): independent-review findings — vendor contracts, profile scope, transport, target binding
Bitwarden unlock now uses the CLI's documented non-interactive channel:
`bw unlock --raw --nointeraction --passwordenv VAR`, VAR set on the child
environment only (bw 2026.x rejects a piped password with "Master password
is required"). Verified against the real published binary.

Manager session tokens are keyed by (profile home, backend): a Desktop
gateway hosting several profiles can no longer reuse or lock another
profile's session. Status probes (`vault.sources`, is_unlocked) no longer
refresh the idle TTL; only real manager calls do. Gateway session teardown
locks the profile's managers (a per-session unlock ends with the session).

1Password service-account token comes from the profile-scoped secret store
(get_secret), not ambient os.environ.

`vault.source.set` no longer references a module constant (bind_module
rebinding dropped it → NameError on every Settings toggle).

Fill target binding: inspection stamps each input with a per-inspection
slot attribute; the fill resolves by stamp and requires type=password, then
strips every stamp. A DOM reflow between inspect and fill can no longer
redirect the password into a text field (reproduced in real Chrome before,
0 filled after).

Redaction boundary: no 4-char floor, CR/LF-normalized form registered
(what a text input actually stores), JSON object KEYS scrubbed in both
browser redactors; longest value first. Docs now state the real trust
model: accidental-disclosure protection, not an execution sandbox.

Desktop: the mid-turn card sends the master password through the owning
session's socket (requestForOwnedSession), never the ambient foreground
gateway; `vault.unlock.expire` clears a stale card; Settings keeps the
master password out of react-query mutation variables (ref consumed by the
mutationFn). One renderer invariant test for the routing.
2026-09-10 10:35:07 -07:00
Teknium ac07e20407 fix: resolve subagent control authority from the live session slot
Subagent list/tail/steer/interrupt authorized against a per-record copy of
the owning session's transport (`owner_transport`). That copy had to be
re-synced at every reattach site; `_rebind_live_transport` did it for
session.resume/activate but prompt.submit and the queued-prompt drain still
attached bare, so a client that reconnected through a prompt (the common
path on a remote gateway / Bot Mode switch) streamed fine while
`subagent.list` returned [] and controls rejected.

Read `owner_session_record["transport"]` at check time instead: the slot is
already mutated by every attach/detach/viewer-failover path, so no site can
forget the sync. `owner_transport` stays as the capture-time "commissioned
by a gateway session" marker (None = no RPC authority ever); non-dict owners
keep the exact-object rule. Drops the registration-time re-read and the
attach-time registry loop.

Diagnosis credit: nftpoetrist (#106663) — their prompt.submit / drain
regression tests pass against this change with no call-site edits.
2026-09-10 10:23:12 -07:00
Teknium fb5715c19f fix: preserve subagent controls across attached session peers 2026-09-08 03:06:30 -07:00
Teknium 7befa11bf2 fix: retain subagent control after live session reattachment 2026-09-08 03:06:30 -07:00
Ryan Tucker 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.
2026-09-07 22:25:12 -07:00
FalconOrtiz db96c8ced7 fix: admit Bot Chat deliveries through the live session owner
Adapt FalconOrtiz's owner-mailbox proposal from #101564 onto the current
notification poller and topical modules. The durable mailbox is cross-process
ingress only: the existing owner admits its normal prompt turn after the
current turn and human FIFO clear. Retain immutable receipts, stable admission
identities, capability and lease fencing, compression lineage, and disable
blind recovery replay of imported turns.

The original stale server hunks and expiring receipt protocol were rebuilt
rather than cherry-picked: the current facade decomposition and durable busy
admission contract differ. Credit the earlier owner-mailbox work in #100544
and durable producer work in #100319.

Co-authored-by: fangliquanflq <fangliquan@qq.com>
Co-authored-by: 686f6c61 <github@00b.tech>
2026-09-07 16:48:29 -07:00
teknium1 c22588bc79 fix(tui-gateway): start each detachment with a fresh settlement budget 2026-09-07 07:05:21 -07:00
fangliquanflq f2c3455e67 fix(tui-gateway): clear abandoned orphan interrupt claims 2026-09-07 07:05:21 -07:00
Teknium 44a583fcc8 fix: retain profile idle activity after session removal 2026-09-07 04:56:38 -07:00
Mark S. 77915e344c fix(tui-gateway): preserve active isolated turns after disconnect 2026-09-06 20:26:11 +05:30
kshitijk4poor 01ae7a5668 refactor(tui-gateway): _rebind_live_transport lives with the reattach contract in session_lifecycle
It is lifecycle logic (detached sentinel, reap cancel) and its siblings
_reattach_refusal / _cancel_ws_orphan_reap are already there; server.py is the
facade and gets no new definitions. Also trims the _claim_or_reuse_live comment
to point at where the reap cancel now happens.
2026-09-06 14:14:29 +05:30
kshitijk4poor f2077a0209 refactor(tui-gateway): one _reattach_refusal helper for the live-identity/interrupt-settling guard
The 4007/4009 guard the salvaged fix added to prompt.submit, session.activate,
_resume_live_unpersisted and _resume_reuse_live_locked was the same six lines
four times. One helper in session_lifecycle.py (next to _cancel_ws_orphan_reap,
whose contract it mirrors) so the next reattach path cannot drift from the others.
Behaviour unchanged; the 19 race tests cover all four call sites.
2026-09-06 14:14:29 +05:30
BearHuddleston 8513c5984c [verified] fix(gateway): fence orphan timers and reconnect interrupt claims
Re-scope #98106 onto the activity-based orphan policy from #100504. Keep timer ownership across callbacks and continuations, serialize reconnect paths against interrupt claims, and avoid recursive eager-resume locking. Leave cleanup polling armed when concurrent cold reuse is rejected.
2026-09-06 14:14:29 +05:30
Teknium e83816a4d1 review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
2026-09-03 09:44:26 -07:00
Teknium 93cf6b3dce Merge simp/r3-37 (late tail) into hermes/simplify-codebase 2026-09-03 04:35:51 -07:00
Teknium 0071ba9965 Merge origin/main (561b053f79) into simp/forwardport: forward-port 220 main commits into the simplified tree 2026-09-03 03:31:03 -07:00
Teknium e2a79dd09b refactor(tui): pass-2 compaction of session lifecycle/workdir/notifications/auto-continue split modules 2026-09-03 01:21:49 -07:00
Teknium e4cec29a52 refactor(tui): session_lifecycle/workdir/reaper — lease retry helper, fold single-caller helpers, drop empty registries, compact docstrings 2026-09-02 23:50:58 -07:00
Teknium e9f2ef41d5 refactor(tui): session_lifecycle/workdir/reaper — fold single-use helpers, unify daemon-timer + memo, collapse defensive layers (-12% LOC) 2026-09-02 23:15:39 -07:00
Teknium 99433742dc refactor(tui): extract the prompt turn into prompt_turn.py; compact methods_*/session_*/agent_callbacks; split _start_agent_build into scope/wiring helpers
- server.py 7663 -> 5319: _run_prompt_submit and its goal/loop/voice/scope
  phases move to tui_gateway/prompt_turn.py (bound via method_ctx.bind_module);
  _start_agent_build split into _bind/_release_build_profile_scopes,
  _deferred_build_agent_kwargs, _wire_session_agent, _start_session_services;
  _load_enabled_toolsets split (_enabled_mcp_server_names, _resolve_explicit_toolsets).
- methods_slash: _LIVE_SLASH_OUTPUT dispatch table; methods_tools: _SLASH_BUILTINS,
  _guarded; tool_progress: _PROGRESS_HANDLERS; methods_config_set:
  _REASONING_DISPLAY_WORDS; methods_voice: _VOICE_TOGGLE_ACTIONS.
- Unified: _denied_source (methods_session) replaces _WORKER_SOURCES in
  methods_profiles; _compress_live_with_feedback / _compute_host_slash shared by
  the slash mirror and /compress; _end_voice_chat shared by stop-phrase paths;
  _watcher_mtime_ns; _reaper_session_is_detached_idle; _notif_* helpers.
- Dead: _profile_dir_or_err, _resume_info, _slash_builtin_table (refs.py: 0 hits).
- acp_adapter/server.py: docstring compaction only (AST-identical).
- Every file semantically reviewed hunk-by-hunk for wire/log/lock/order parity.
2026-09-02 14:09:13 -07:00
Teknium c6011955df refactor(tui): split compute-host bridge, session workdir/db-row, and session lifecycle out of server.py 2026-09-02 14:09:13 -07:00