AITURK IDE 1.0.0-beta.18: gorunmeyen onay beklemesi yerine aninda "onay gerekli"
A one-shot run (hermes chat --query-file ... -Q, stdin closed, no window)
still installs the prompt_toolkit approval callback but never starts the
prompt UI. The protected instruction-file gate (AGENTS.md / CLAUDE.md /
SOUL.md ...) took the registered callback for a reachable human and waited
out approvals.timeout on a panel painted nowhere. With the profile value of
300000 s the stage job hung for good.
- Gate: in a one-shot run, or when only an automatic approver
(subagent_auto_approve) is attached, return BLOCKED / APPROVAL REQUIRED
immediately instead of prompting. Human consent is still required every
time; yolo and approvals.single_query_mode still do not open this gate.
- One-shot exit prints an "approval_required: {json}" line on stderr
(session_id, tool_call_id, request_id, targets, reason, applied=false)
so the launcher can tell "blocked, needs a human" from "still working".
- execute_code: an approval that arrives after the calling script stopped
waiting (300 s reply timeout, script run timeout, retired kernel cell)
is not applied.
- Same invisible wait removed from the computer_use and memory-write
prompts in a one-shot run. The one-shot marker is no longer inherited by
child processes.
- BLOCKED text no longer breaks on a target path containing braces.
Long-running tools are untouched: no general tool timeout was shortened.
Kart: turkservis-20261007-1421-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwSpve9HuXMy7dHf4RDMzX
This commit is contained in:
1 parent
58dbb4523b
commit
7e4cc0915c
13 files changed
+886
-16
No files matched your search
@@ -78,6 +78,115 @@ _hermes_interactive_ctx: contextvars.ContextVar[Optional[str]] = contextvars.Con
|
||||
)
|
||||
|
||||
|
||||
# Absolute ``time.monotonic()`` instant after which whoever issued the
|
||||
# current tool call has stopped waiting for its result. Set by the
|
||||
# execute_code RPC dispatchers, whose sandbox client gives up after a fixed
|
||||
# reply timeout. An approval that resolves after this instant must NOT be
|
||||
# acted on: the caller already reported the call as failed, so applying it
|
||||
# would be a write the agent believes never happened (and may redo).
|
||||
# The reply timeout is not the only way a caller goes away: the script can
|
||||
# hit its own run timeout or be interrupted first. ``gone`` is a probe the
|
||||
# dispatcher supplies for that ("has my script/cell already ended?").
|
||||
# Thread-local on purpose: the dispatcher and the tool handler it calls run
|
||||
# on the same thread, whatever context the handler is re-bound to.
|
||||
_approval_reply_deadline = threading.local()
|
||||
|
||||
|
||||
def set_approval_reply_deadline(deadline: Optional[float], gone=None):
|
||||
"""Bind the caller's reply window for this thread; return the prior binding.
|
||||
|
||||
``deadline`` is monotonic seconds (None = unbounded); ``gone`` is an
|
||||
optional zero-argument callable that turns true once the caller has
|
||||
ended. Pass the returned value to :func:`restore_approval_reply_deadline`.
|
||||
"""
|
||||
previous = (
|
||||
getattr(_approval_reply_deadline, "value", None),
|
||||
getattr(_approval_reply_deadline, "gone", None),
|
||||
)
|
||||
_approval_reply_deadline.value = deadline
|
||||
_approval_reply_deadline.gone = gone
|
||||
return previous
|
||||
|
||||
|
||||
def restore_approval_reply_deadline(previous) -> None:
|
||||
"""Undo :func:`set_approval_reply_deadline` / :func:`also_gone_when`."""
|
||||
deadline, gone = previous if previous else (None, None)
|
||||
_approval_reply_deadline.value = deadline
|
||||
_approval_reply_deadline.gone = gone
|
||||
|
||||
|
||||
def also_gone_when(probe):
|
||||
"""Add another "caller has ended" probe, keeping the current window."""
|
||||
deadline = getattr(_approval_reply_deadline, "value", None)
|
||||
current = getattr(_approval_reply_deadline, "gone", None)
|
||||
|
||||
def gone() -> bool:
|
||||
return bool(probe()) or bool(current and current())
|
||||
|
||||
return set_approval_reply_deadline(deadline, gone)
|
||||
|
||||
|
||||
def approval_reply_window_remaining() -> Optional[float]:
|
||||
"""Seconds left before the caller stops waiting, or None when unbounded."""
|
||||
deadline = getattr(_approval_reply_deadline, "value", None)
|
||||
if deadline is None:
|
||||
return None
|
||||
return deadline - time.monotonic()
|
||||
|
||||
|
||||
def approval_arrived_too_late() -> bool:
|
||||
"""True when the caller of the current tool call has stopped waiting."""
|
||||
gone = getattr(_approval_reply_deadline, "gone", None)
|
||||
if gone is not None:
|
||||
try:
|
||||
if gone():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
remaining = approval_reply_window_remaining()
|
||||
return remaining is not None and remaining <= 0
|
||||
|
||||
|
||||
# Approvals that could not be put in front of a human because the run has
|
||||
# no interactive channel (single-query ``chat -Q`` stage runs, scripts).
|
||||
# The single-query CLI reports them on stderr as ``approval_required:``
|
||||
# lines so the coordinating process gets a machine-readable outcome
|
||||
# instead of inferring one from prose. Bounded: long-lived processes that
|
||||
# never drain it must not grow without limit.
|
||||
_human_approval_required: list[dict] = []
|
||||
_HUMAN_APPROVAL_REQUIRED_MAX = 50
|
||||
_human_approval_required_lock = threading.Lock()
|
||||
|
||||
|
||||
def note_human_approval_required(kind: str, targets: list[str], reason: str) -> dict:
|
||||
"""Record an approval the run could not present to a human.
|
||||
|
||||
Captures the active session/tool-call identity so the coordinator can
|
||||
tie the outcome to the exact job and resume the same session.
|
||||
"""
|
||||
record = {
|
||||
"kind": kind,
|
||||
"targets": list(dict.fromkeys(str(t) for t in targets)),
|
||||
"reason": reason,
|
||||
"request_id": uuid.uuid4().hex,
|
||||
"tool_call_id": _approval_tool_call_id.get() or "",
|
||||
"session_id": _approval_session_id.get() or os.getenv("HERMES_SESSION_ID", ""),
|
||||
"applied": False,
|
||||
}
|
||||
with _human_approval_required_lock:
|
||||
_human_approval_required.append(record)
|
||||
del _human_approval_required[:-_HUMAN_APPROVAL_REQUIRED_MAX]
|
||||
return dict(record)
|
||||
|
||||
|
||||
def drain_human_approval_required() -> list[dict]:
|
||||
"""Return and clear the recorded approval-required outcomes."""
|
||||
with _human_approval_required_lock:
|
||||
records = list(_human_approval_required)
|
||||
_human_approval_required.clear()
|
||||
return records
|
||||
|
||||
|
||||
def set_hermes_interactive_context(interactive: bool) -> contextvars.Token:
|
||||
"""Bind interactive mode for the current context (thread or asyncio task).
|
||||
|
||||
|
||||
Reference in new issue
Block a user