mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix: what the main merge left broken outside its conflict hunks
- Desktop input-requests: main's new `else clearParkedCard(ctx, id)` merged cleanly into this branch's extracted cancelParkedCredentialRequest, which had no ctx (tsc TS2304). The helper takes the event context. - Composer: with a card waiting, Send answers it, so the primary button reads Send whatever the busy action (main's controls.test "answers a waiting card with Send"). The queue-button gate moves to a helper to stay inside the component's complexity cap. - tests/e2e/test_relay_native_openai_stream.py: main fixed the same finalizer deadlock its own way (6e84ce66fb: wait only off the stream's loop); the merge kept main's wait but this branch's removal of its Event (F821). Take main's file. - hermes_state_sessions.py went to 2005 lines (cap 2000) with main's two conversation model_config helpers on top of this branch's growth: their bodies move to hermes_state_sessions_model_config.py, the methods stay as thin delegates. - overlay-gaps.json: drop the known gap for connectors.directory.receiptSaid, which the English catalog no longer has (overlay-completeness test). Checks: desktop typecheck 0 errors; vitest 1736 + 16 passed; pytest 1243 + 124 + 10 passed; scripts/check health 0 blocking vs0e427d788d.
This commit is contained in:
1 parent
0715474bfd
commit
8dcf72feb5
8 files changed
+85
-51
No files matched your search
@@ -87,10 +87,11 @@ export function ComposerControls({
|
||||
// Steer is just send: a payload keeps the Send affordance mid-turn (it answers
|
||||
// a waiting card). Stop only when the composer is empty and a turn is running.
|
||||
const showStop = busy && !hasComposerPayload
|
||||
const showQueueButton =
|
||||
busy && busyAction !== 'stop' && busyAction !== 'queue' && hasComposerPayload && !cardWaiting
|
||||
|
||||
const sendLabel = composerSendLabel(busy, busyAction, c)
|
||||
const showQueueButton = queueButtonVisible(busy, busyAction, hasComposerPayload, cardWaiting)
|
||||
|
||||
// A waiting card takes the draft as its answer through Send, whatever the busy action.
|
||||
const sendLabel = composerSendLabel(busy && !cardWaiting, busyAction, c)
|
||||
|
||||
// The HUD is a Spotlight bar a few hundred pixels wide, so the four separate
|
||||
// voice toggles fold into one menu there and leave the row to the input. A
|
||||
@@ -191,6 +192,17 @@ export function ComposerControls({
|
||||
)
|
||||
}
|
||||
|
||||
// Queue is the secondary mid-turn action: never while the primary button already queues or stops, and
|
||||
// never while a card waits (its Send answers the card).
|
||||
function queueButtonVisible(
|
||||
busy: boolean,
|
||||
busyAction: 'interrupt' | 'steer' | 'queue' | 'stop',
|
||||
hasPayload: boolean,
|
||||
cardWaiting: boolean
|
||||
): boolean {
|
||||
return busy && busyAction !== 'stop' && busyAction !== 'queue' && hasPayload && !cardWaiting
|
||||
}
|
||||
|
||||
function composerSendLabel(
|
||||
busy: boolean,
|
||||
busyAction: 'interrupt' | 'steer' | 'queue' | 'stop',
|
||||
|
||||
+5
-3
@@ -232,14 +232,16 @@ export function handleInputRequestEvent(ctx: GatewayEventContext): boolean {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
cancelParkedCredentialRequest(key, sessionId, id)
|
||||
cancelParkedCredentialRequest(ctx, key, id)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** Withdraw the sudo / secret / vault card parked under `id`, first match wins. */
|
||||
function cancelParkedCredentialRequest(key: string, sessionId: GatewayEventContext['sessionId'], id: string): void {
|
||||
/** Withdraw the sudo / secret / vault / setup card parked under `id`, first match wins. */
|
||||
function cancelParkedCredentialRequest(ctx: GatewayEventContext, key: string, id: string): void {
|
||||
const { sessionId } = ctx
|
||||
|
||||
if ($sudoRequests.get()[key]?.requestId === id) {
|
||||
clearSudoRequest(sessionId, id)
|
||||
} else if ($sudoRequests.get()['']?.requestId === id) {
|
||||
|
||||
@@ -211,10 +211,7 @@ export function useSlashCommand(deps: SlashCommandDeps) {
|
||||
const compressInFlightRef = useRef(new Set<string>())
|
||||
|
||||
return useCallback(
|
||||
async (
|
||||
rawCommand: string,
|
||||
options?: SubmitTextOptions & { recordInput?: boolean; typed?: boolean }
|
||||
) => {
|
||||
async (rawCommand: string, options?: SubmitTextOptions & { recordInput?: boolean; typed?: boolean }) => {
|
||||
const initialRuntimeId = options?.sessionId ?? activeSessionIdRef.current
|
||||
const initialSelectedId = selectedStoredSessionIdRef.current
|
||||
const initialRoutedId = getRoutedStoredSessionId()
|
||||
@@ -238,16 +235,18 @@ export function useSlashCommand(deps: SlashCommandDeps) {
|
||||
// An explicit retry of this exact invocation reuses its retained expansion; the same
|
||||
// command typed again is a new invocation.
|
||||
const prepared = options?.submission_id
|
||||
? (await adoptPreparedSubmission(
|
||||
preparedSubmissionKey(
|
||||
resolveComposerSessionKey(initialStoredId ?? initialRuntimeId, $sessions.get()),
|
||||
destination,
|
||||
rawCommand,
|
||||
options?.attachments ?? $composerAttachments.get(),
|
||||
retryOptions
|
||||
),
|
||||
options.submission_id
|
||||
))?.entry
|
||||
? (
|
||||
await adoptPreparedSubmission(
|
||||
preparedSubmissionKey(
|
||||
resolveComposerSessionKey(initialStoredId ?? initialRuntimeId, $sessions.get()),
|
||||
destination,
|
||||
rawCommand,
|
||||
options?.attachments ?? $composerAttachments.get(),
|
||||
retryOptions
|
||||
),
|
||||
options.submission_id
|
||||
)
|
||||
)?.entry
|
||||
: undefined
|
||||
|
||||
if (prepared) {
|
||||
|
||||
@@ -2691,7 +2691,9 @@ export function useSessionActions({
|
||||
// Stable per-attempt key: a lost-response retry of session.branch /
|
||||
// session.branch_whole returns the SAME child (#65410).
|
||||
idempotency_key: key,
|
||||
...(branchCount !== undefined ? { count: branchCount, through_message_id: branchThroughRowId(branchMessages) } : {})
|
||||
...(branchCount !== undefined
|
||||
? { count: branchCount, through_message_id: branchThroughRowId(branchMessages) }
|
||||
: {})
|
||||
}
|
||||
|
||||
const createParams = {
|
||||
|
||||
@@ -320,7 +320,6 @@
|
||||
"connectors.directory.allApps": ["ar","ja","ru","zh-hant"],
|
||||
"connectors.directory.receiptConnected": ["ar","ja","ru","zh-hant"],
|
||||
"connectors.directory.receiptNone": ["ar","ja","ru","zh-hant"],
|
||||
"connectors.directory.receiptSaid": ["ar","ja","ru","zh-hant"],
|
||||
"connectors.directory.receiptTimedOut": ["ar","ja","ru","zh-hant"],
|
||||
"connectors.directory.suggested": ["ar","ja","ru","zh-hant"],
|
||||
"connectors.disabled": ["ar","ja","ru","zh-hant"],
|
||||
|
||||
@@ -783,32 +783,14 @@ class SessionSessionsMixin:
|
||||
|
||||
def update_conversation_model_config_value(self, session_id: str, key: str,
|
||||
update: Callable[[Any], Any]) -> Optional[str]:
|
||||
"""Read a conversation-wide ``key`` at *session_id*'s compression tip and write ``update(value)``
|
||||
back, in one write transaction: a concurrent writer of the key cannot be overwritten with a stale
|
||||
copy, and a rotation cannot publish between finding the tip and writing it (the child would keep
|
||||
the old copy). ``update`` may run more than once (lock retry) and must be pure; returning None
|
||||
removes the key. Returns the id written, None when the row does not exist."""
|
||||
def _do(conn) -> Optional[str]:
|
||||
tip = self._compression_tip_on(conn, session_id)
|
||||
row = conn.execute("SELECT model_config FROM sessions WHERE id = ?", (tip,)).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
config = _parse_model_config(row[0])
|
||||
if (value := update(config.get(key))) is None:
|
||||
config.pop(key, None)
|
||||
else:
|
||||
config[key] = value
|
||||
conn.execute("UPDATE sessions SET model_config = ? WHERE id = ?", (json.dumps(config), tip))
|
||||
return tip
|
||||
return self._execute_write(_do)
|
||||
"""Write ``update(value)`` of a conversation-wide key at the compression tip (one transaction)."""
|
||||
from hermes_state_sessions_model_config import update_conversation_model_config_value
|
||||
return update_conversation_model_config_value(self, session_id, key, update)
|
||||
|
||||
def session_ids_with_model_config_key(self, key: str) -> list[str]:
|
||||
"""Ids of the sessions whose model_config carries ``key``, newest first."""
|
||||
with self._read_ctx() as conn:
|
||||
rows = conn.execute(
|
||||
f"SELECT id FROM sessions WHERE {_sql_json_extract('model_config', '$.' + key)} IS NOT NULL "
|
||||
"ORDER BY started_at DESC").fetchall()
|
||||
return [row["id"] for row in rows]
|
||||
from hermes_state_sessions_model_config import session_ids_with_model_config_key
|
||||
return session_ids_with_model_config_key(self, key)
|
||||
|
||||
def update_session_runtime_lock(
|
||||
self, session_id: str, *, model: Optional[str] = None, provider: Optional[str] = None,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Conversation-wide ``sessions.model_config`` keys: read-modify-write at the compression tip and the
|
||||
key scan. Bodies of :class:`hermes_state_sessions.SessionSessionsMixin`'s two methods of the same names."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from hermes_state_common import _sql_json_extract
|
||||
|
||||
|
||||
def update_conversation_model_config_value(db, session_id: str, key: str,
|
||||
update: Callable[[Any], Any]) -> Optional[str]:
|
||||
"""Read a conversation-wide ``key`` at *session_id*'s compression tip and write ``update(value)``
|
||||
back, in one write transaction: a concurrent writer of the key cannot be overwritten with a stale
|
||||
copy, and a rotation cannot publish between finding the tip and writing it (the child would keep
|
||||
the old copy). ``update`` may run more than once (lock retry) and must be pure; returning None
|
||||
removes the key. Returns the id written, None when the row does not exist."""
|
||||
from hermes_state_sessions import _parse_model_config
|
||||
|
||||
def _do(conn) -> Optional[str]:
|
||||
tip = db._compression_tip_on(conn, session_id)
|
||||
row = conn.execute("SELECT model_config FROM sessions WHERE id = ?", (tip,)).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
config = _parse_model_config(row[0])
|
||||
if (value := update(config.get(key))) is None:
|
||||
config.pop(key, None)
|
||||
else:
|
||||
config[key] = value
|
||||
conn.execute("UPDATE sessions SET model_config = ? WHERE id = ?", (json.dumps(config), tip))
|
||||
return tip
|
||||
return db._execute_write(_do)
|
||||
|
||||
|
||||
def session_ids_with_model_config_key(db, key: str) -> list[str]:
|
||||
"""Ids of the sessions whose model_config carries ``key``, newest first."""
|
||||
with db._read_ctx() as conn:
|
||||
rows = conn.execute(
|
||||
f"SELECT id FROM sessions WHERE {_sql_json_extract('model_config', '$.' + key)} IS NOT NULL "
|
||||
"ORDER BY started_at DESC").fetchall()
|
||||
return [row["id"] for row in rows]
|
||||
@@ -63,15 +63,10 @@ def _stream_through_relay(tmp_path, monkeypatch, response_body: bytes, *, finali
|
||||
subscriber_name = "test.openai_stream"
|
||||
events = []
|
||||
relay_finalizer_started = threading.Event()
|
||||
allow_relay_finalizer = threading.Event()
|
||||
relay_finalizer_finished = threading.Event()
|
||||
run_relay_finalizer = relay_llm.ManagedLlmStream._relay_finalizer
|
||||
|
||||
# The finalizer is never gated. Relay runs it on the stream's own loop, which only the consumer
|
||||
# thread drives (``__next__`` -> ``run_until_complete``), and Relay may reach EOF and finalize
|
||||
# BEFORE it hands over the terminal chunk. A finalizer that waited for the consumer then parked the
|
||||
# only thread that could release it: a 30 s deadlock, then "consumer did not release Relay's
|
||||
# finalizer". Finalizing first already IS the ordering under test; the consumer-first ordering is
|
||||
# forced below by pumping the loop until the finalizer has finished.
|
||||
def run_synchronized_relay_finalizer(managed_stream, attempt):
|
||||
relay_finalizer_started.set()
|
||||
try:
|
||||
@@ -100,6 +95,7 @@ def _stream_through_relay(tmp_path, monkeypatch, response_body: bytes, *, finali
|
||||
import asyncio
|
||||
|
||||
stream = self.managed_stream_holder["stream"]
|
||||
allow_relay_finalizer.set()
|
||||
# A schedule probe may hold the provider generator at this chunk until the consumer has
|
||||
# taken it (adverse consumer-first ordering); release it so the pump below can reach EOF.
|
||||
gate = getattr(stream, "_review_gate", None)
|
||||
|
||||
Reference in new issue
Block a user