Compare commits
227 Commits
worktree-d
...
feat/opent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcf49f313e | ||
|
|
6c76908fde | ||
|
|
7776aeb064 | ||
|
|
ad16ec9c53 | ||
|
|
de446a26a5 | ||
|
|
af1e4bb9ab | ||
|
|
22792d2791 | ||
|
|
cbe703cf48 | ||
|
|
5c6438fd28 | ||
|
|
448e6ee68f | ||
|
|
805e08081f | ||
|
|
fe50861c2e | ||
|
|
e3973050df | ||
|
|
a939c9a712 | ||
|
|
e35d953a45 | ||
|
|
197d499480 | ||
|
|
14ee1a52c0 | ||
|
|
50e34713b6 | ||
|
|
e9af6a5110 | ||
|
|
4f66a7cf09 | ||
|
|
5f997247d9 | ||
|
|
ccc89a327d | ||
|
|
408789d909 | ||
|
|
a089614451 | ||
|
|
7592b996a6 | ||
|
|
380f0b53dd | ||
|
|
62537a99bf | ||
|
|
ee4fb837ed | ||
|
|
ddf4cca5c0 | ||
|
|
9122ffffc5 | ||
|
|
ebb58f750c | ||
|
|
b957dc6f72 | ||
|
|
018c8fb17f | ||
|
|
7e3936f47d | ||
|
|
2f666d2e9b | ||
|
|
c146a69b1d | ||
|
|
773690b1f7 | ||
|
|
ddfff88a58 | ||
|
|
443a1be509 | ||
|
|
d96657e2dc | ||
|
|
0e65d54b6d | ||
|
|
3ebcc3439e | ||
|
|
f86bc5170a | ||
|
|
529d8084be | ||
|
|
daa4412378 | ||
|
|
7ad05a3129 | ||
|
|
4d1d1e8f52 | ||
|
|
0bceb219e6 | ||
|
|
579fb58e86 | ||
|
|
91df325458 | ||
|
|
43b096eedb | ||
|
|
394f45a3d5 | ||
|
|
6a6693b182 | ||
|
|
03b16c51a6 | ||
|
|
ac84fe7ea1 | ||
|
|
afe5152314 | ||
|
|
5fd2b5bb7b | ||
|
|
0bde6a890f | ||
|
|
e17e94c8de | ||
|
|
99d163a8ae | ||
|
|
b537a3ba50 | ||
|
|
e7e8c820fc | ||
|
|
8c26b14931 | ||
|
|
a38152cd91 | ||
|
|
7af4055ddc | ||
|
|
de9f3effbb | ||
|
|
ac7ab6c0c0 | ||
|
|
8580172d11 | ||
|
|
af98e6deef | ||
|
|
52aa2f98f9 | ||
|
|
fb1fb1e5ca | ||
|
|
87b33cb10c | ||
|
|
51031ec655 | ||
|
|
edc6e67add | ||
|
|
2d3cf85d67 | ||
|
|
8f112b0633 | ||
|
|
216790a8f8 | ||
|
|
2a25c1c40b | ||
|
|
d0b14bc6ef | ||
|
|
c70620e4a0 | ||
|
|
2b1564199c | ||
|
|
fdc0e5fea5 | ||
|
|
b3d2de87f9 | ||
|
|
cff7b365d2 | ||
|
|
0240299fb0 | ||
|
|
2f30c09378 | ||
|
|
90840708f1 | ||
|
|
60f47eab37 | ||
|
|
04704c103e | ||
|
|
6d2211d9d0 | ||
|
|
cdeef30c62 | ||
|
|
3d3fc24d9a | ||
|
|
2e31140728 | ||
|
|
f4a83c9298 | ||
|
|
00cb21de3e | ||
|
|
0437dd060c | ||
|
|
bc9447d23b | ||
|
|
79dc862680 | ||
|
|
01fa8dcc00 | ||
|
|
3882cc6e61 | ||
|
|
6b87243ecd | ||
|
|
49d90e68c6 | ||
|
|
2cd122c9c1 | ||
|
|
53438228ee | ||
|
|
503c1201ff | ||
|
|
e44b43ad16 | ||
|
|
cd09aa61ef | ||
|
|
c507ca6b3b | ||
|
|
d90e195670 | ||
|
|
793462a395 | ||
|
|
636bb6e928 | ||
|
|
0da48b0c7f | ||
|
|
50023fd151 | ||
|
|
5352aec064 | ||
|
|
7b14c51e7b | ||
|
|
8c1b62e72f | ||
|
|
e136314039 | ||
|
|
73d5c2871d | ||
|
|
d46a8f4492 | ||
|
|
eaee382b47 | ||
|
|
59e9e6a26e | ||
|
|
a046cee754 | ||
|
|
15ccaf9ab9 | ||
|
|
c391add579 | ||
|
|
1e55b3b294 | ||
|
|
76cf809066 | ||
|
|
915b9b5f6f | ||
|
|
1bf9dff1fb | ||
|
|
e14dfa86c6 | ||
|
|
055bc3e3a2 | ||
|
|
c019a9d2d5 | ||
|
|
99b24f6747 | ||
|
|
d4d7c9b0ae | ||
|
|
ba10594322 | ||
|
|
0d0e9203cf | ||
|
|
abdc21f39a | ||
|
|
87634e19fd | ||
|
|
d01b573796 | ||
|
|
a572a1eae4 | ||
|
|
b72ac77783 | ||
|
|
53b37463c4 | ||
|
|
cc2c881fd1 | ||
|
|
12342a4bce | ||
|
|
ea0de82422 | ||
|
|
e45b745835 | ||
|
|
e02f4c03c3 | ||
|
|
3714caa1b9 | ||
|
|
329c33dac3 | ||
|
|
d759c13c09 | ||
|
|
694adec635 | ||
|
|
f0fcaa1e54 | ||
|
|
0f500fc41d | ||
|
|
3fc67b7333 | ||
|
|
ede4f5a4a3 | ||
|
|
9d6992ee8a | ||
|
|
1c68f6f81f | ||
|
|
6459b3d991 | ||
|
|
1a626470ca | ||
|
|
524453dab5 | ||
|
|
4d926f248d | ||
|
|
648706936d | ||
|
|
39c4ac3af1 | ||
|
|
cb5c24e37d | ||
|
|
8e223b36ed | ||
|
|
777dc9da62 | ||
|
|
240c5d4543 | ||
|
|
132d6fe6d6 | ||
|
|
f5bd09af4b | ||
|
|
9b631e4ae1 | ||
|
|
2789bf4e25 | ||
|
|
568e127612 | ||
|
|
4da45e8727 | ||
|
|
b2e6053243 | ||
|
|
54870847cb | ||
|
|
86c537d209 | ||
|
|
2a10da3a16 | ||
|
|
b8469a81e3 | ||
|
|
2e62862784 | ||
|
|
b5f7a1f299 | ||
|
|
cca3b77a4b | ||
|
|
8513a6aec7 | ||
|
|
ad8e57793d | ||
|
|
5408013369 | ||
|
|
a77bc2c08d | ||
|
|
48ae8029aa | ||
|
|
bddc5fd087 | ||
|
|
53a2ac8f2d | ||
|
|
ace4b722dc | ||
|
|
0c67d4015f | ||
|
|
78e2101cd2 | ||
|
|
e53b74c394 | ||
|
|
09a5548628 | ||
|
|
2e61de0638 | ||
|
|
f1d3afb151 | ||
|
|
9513793ad7 | ||
|
|
41f0714287 | ||
|
|
18c085b1a4 | ||
|
|
b18490b890 | ||
|
|
38d1a414a1 | ||
|
|
09ec26c66a | ||
|
|
ab0a6270c3 | ||
|
|
133e0271e2 | ||
|
|
b5a457c033 | ||
|
|
d65b513f23 | ||
|
|
86e5efb0ae | ||
|
|
ba29010902 | ||
|
|
e3b8b6d32c | ||
|
|
fa42ac094d | ||
|
|
30c7913617 | ||
|
|
d3b670e63e | ||
|
|
b97cd81c78 | ||
|
|
ad399b9229 | ||
|
|
2aa316ec9c | ||
|
|
4ce9caed04 | ||
|
|
6bdc4c0231 | ||
|
|
628780b4f3 | ||
|
|
c50fb560ef | ||
|
|
69a293b419 | ||
|
|
9c5d1afbe9 | ||
|
|
ae82eed2b1 | ||
|
|
cb83149dc6 | ||
|
|
2b119baac1 | ||
|
|
09d66037f8 | ||
|
|
dde9c0d19d | ||
|
|
e029b7597b | ||
|
|
1c7ae46f0e | ||
|
|
cadb74adad |
31
Dockerfile
@@ -1,12 +1,14 @@
|
||||
FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source
|
||||
# Node 22 LTS source stage. Debian trixie's bundled nodejs is pinned to 20.x
|
||||
# which reached EOL in April 2026 — we copy node + npm + corepack from the
|
||||
# upstream node:22 image instead so we can stay on a supported LTS without
|
||||
# waiting for Debian 14 (forky, ~mid-2027). Bookworm-based slim image used
|
||||
# so the produced binary links against glibc 2.36, which runs cleanly on
|
||||
# our Debian 13 (trixie, glibc 2.41) runtime. Bumping to a new Node major
|
||||
# is a one-line ARG change; see #4977.
|
||||
FROM node:22-bookworm-slim@sha256:7af03b14a13c8cdd38e45058fd957bf00a72bbe17feac43b1c15a689c029c732 AS node_source
|
||||
# Node 26 source stage. Debian trixie's bundled nodejs is pinned to 20.x
|
||||
# (EOL April 2026), so we copy node + npm + corepack from the upstream node:26
|
||||
# image instead. Node 26 (Current; LTS promotion ~Oct 2026) is REQUIRED by the
|
||||
# native OpenTUI TUI engine, which loads its renderer via the experimental
|
||||
# `node:ffi` API that only exists on Node 26.3+ (the Ink engine + web build run
|
||||
# on it too). Bookworm-based slim image used so the produced binary links
|
||||
# against glibc 2.36, which runs cleanly on our Debian 13 (trixie, glibc 2.41)
|
||||
# runtime. The pinned tag ships v26.3.0. Bumping Node is a one-line change here.
|
||||
# NOTE: verify the full image build + Ink/web/Playwright on Node 26 in CI.
|
||||
FROM node:26-bookworm-slim@sha256:79723b41edbedf595f62e943a9f8b0ba9af5b1e61045c5f8f59c2c02c1212a16 AS node_source
|
||||
FROM debian:13.4
|
||||
|
||||
# Disable Python stdout buffering to ensure logs are printed immediately
|
||||
@@ -90,7 +92,7 @@ RUN useradd -u 10000 -m -d /opt/data hermes
|
||||
|
||||
COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/
|
||||
|
||||
# Node 22 LTS: copy the node binary plus the bundled npm + corepack JS
|
||||
# Node 26: copy the node binary plus the bundled npm + corepack JS
|
||||
# installs from the upstream image. npm and npx are recreated as symlinks
|
||||
# because they're symlinks in the source image (and need to live on PATH).
|
||||
# See node_source stage at the top of the file for the version-bump
|
||||
@@ -119,7 +121,7 @@ COPY ui-tui/packages/hermes-ink/ ui-tui/packages/hermes-ink/
|
||||
|
||||
# `npm_config_install_links=false` forces npm to install `file:` deps as
|
||||
# symlinks instead of copies. This is the default since npm 10+, which is
|
||||
# what the image ships now (via the node:22 source stage). We set it
|
||||
# what the image ships now (via the node:26 source stage). We set it
|
||||
# explicitly anyway as defense-in-depth: the previous Debian-bundled npm
|
||||
# 9.x defaulted to install-as-copy, which produced a hidden
|
||||
# node_modules/.package-lock.json that permanently disagreed with the root
|
||||
@@ -174,8 +176,15 @@ RUN uv sync --frozen --no-install-project --extra all --extra messaging --extra
|
||||
COPY --chown=hermes:hermes . .
|
||||
|
||||
# Build browser dashboard and terminal UI assets.
|
||||
# ui-opentui is the opt-in native OpenTUI engine (HERMES_TUI_ENGINE=opentui;
|
||||
# default stays Ink). .dockerignore strips its node_modules/dist, so install +
|
||||
# esbuild-build it here → dist/main.js, then prune devDeps (esbuild/babel/vitest);
|
||||
# the runtime only needs the prod deps (the external @opentui/core + its native
|
||||
# blob — the bundle inlines solid/effect). Build needs Node 26.3 (node:ffi floor),
|
||||
# which this image now ships. (CI must verify the full image build on Node 26.)
|
||||
RUN cd web && npm run build && \
|
||||
cd ../ui-tui && npm run build
|
||||
cd ../ui-tui && npm run build && \
|
||||
cd ../ui-opentui && npm install --no-audit --no-fund && npm run build && npm prune --omit=dev
|
||||
|
||||
# ---------- Permissions ----------
|
||||
# Make install dir world-readable so any HERMES_UID can read it at runtime.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<a href="https://github.com/NousResearch/hermes-agent/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License: MIT"></a>
|
||||
<a href="https://nousresearch.com"><img src="https://img.shields.io/badge/Built%20by-Nous%20Research-blueviolet?style=for-the-badge" alt="Built by Nous Research"></a>
|
||||
<a href="README.zh-CN.md"><img src="https://img.shields.io/badge/Lang-中文-red?style=for-the-badge" alt="中文"></a>
|
||||
<a href="README.ur-pk.md"><img src="https://img.shields.io/badge/Lang-اردو-green?style=for-the-badge" alt="اردو"></a>
|
||||
</p>
|
||||
|
||||
**The self-improving AI agent built by [Nous Research](https://nousresearch.com).** It's the only agent with a built-in learning loop — it creates skills from experience, improves them during use, nudges itself to persist knowledge, searches its own past conversations, and builds a deepening model of who you are across sessions. Run it on a $5 VPS, a GPU cluster, or serverless infrastructure that costs nearly nothing when idle. It's not tied to your laptop — talk to it from Telegram while it works on a cloud VM.
|
||||
@@ -104,6 +105,8 @@ You can still bring your own keys per-tool whenever you want — the gateway is
|
||||
|
||||
Hermes has two entry points: start the terminal UI with `hermes`, or run the gateway and talk to it from Telegram, Discord, Slack, WhatsApp, Signal, or Email. Once you're in a conversation, many slash commands are shared across both interfaces.
|
||||
|
||||
> **TUI engine:** On supported hosts (Linux/macOS with Node 26.3+), the terminal UI defaults to the native **OpenTUI** engine, which the installer provisions for you. The legacy **Ink** engine remains the fallback — it's used automatically on Windows, Termux, or when the native engine can't run, and you can select it explicitly with `HERMES_TUI_ENGINE=ink hermes`. Ink is not going away; it's the kept fallback.
|
||||
|
||||
| Action | CLI | Messaging platforms |
|
||||
| ------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| Start chatting | `hermes` | Run `hermes gateway setup` + `hermes gateway start`, then send the bot a message |
|
||||
|
||||
261
README.ur-pk.md
Normal file
@@ -0,0 +1,261 @@
|
||||
<div dir="rtl">
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/banner.png" alt="Hermes Agent" width="100%">
|
||||
</p>
|
||||
|
||||
# ہرمیس ایجنٹ ☤ (Hermes Agent)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://hermes-agent.nousresearch.com/docs/"><img src="https://img.shields.io/badge/Docs-hermes--agent.nousresearch.com-FFD700?style=for-the-badge" alt="Documentation"></a>
|
||||
<a href="https://discord.gg/NousResearch"><img src="https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
|
||||
<a href="https://github.com/NousResearch/hermes-agent/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License: MIT"></a>
|
||||
<a href="https://nousresearch.com"><img src="https://img.shields.io/badge/Built%20by-Nous%20Research-blueviolet?style=for-the-badge" alt="Built by Nous Research"></a>
|
||||
<a href="README.md"><img src="https://img.shields.io/badge/Lang-English-lightgrey?style=for-the-badge" alt="English"></a>
|
||||
<a href="README.zh-CN.md"><img src="https://img.shields.io/badge/Lang-中文-red?style=for-the-badge" alt="中文"></a>
|
||||
</p>
|
||||
|
||||
**[نوس ریسرچ (Nous Research)](https://nousresearch.com) کا تیار کردہ خود کو بہتر بنانے والا اے آئی (AI) ایجنٹ۔** یہ واحد ایجنٹ ہے جس میں سیکھنے کا عمل (learning loop) پہلے سے موجود ہے — یہ اپنے تجربات سے نئی مہارتیں (skills) بناتا ہے، استعمال کے دوران ان کو بہتر کرتا ہے، معلومات کو محفوظ رکھنے کے لیے خود کو یاد دہانی کرواتا ہے، اپنی پرانی بات چیت کو تلاش کر سکتا ہے، اور مختلف سیشنز کے دوران آپ کے بارے میں ایک گہری سمجھ پیدا کرتا ہے۔ اسے $5 والے VPS پر چلائیں، GPU کلسٹر پر، یا سرور لیس (serverless) انفراسٹرکچر پر جس کی قیمت استعمال نہ ہونے پر تقریباً صفر ہے۔ یہ آپ کے لیپ ٹاپ تک محدود نہیں ہے — آپ ٹیلی گرام (Telegram) سے اس کے ساتھ بات چیت کر سکتے ہیں جبکہ یہ کلاؤڈ VM پر کام کر رہا ہو۔
|
||||
|
||||
آپ اپنی مرضی کا کوئی بھی ماڈل استعمال کر سکتے ہیں — [Nous Portal](https://portal.nousresearch.com)، [OpenRouter](https://openrouter.ai) (200 سے زائد ماڈلز)، [NovitaAI](https://novita.ai) (ماڈل API، ایجنٹ سینڈ باکس، اور GPU کلاؤڈ کے لیے اے آئی مقامی کلاؤڈ)، [NVIDIA NIM](https://build.nvidia.com) (Nemotron)، [Xiaomi MiMo](https://platform.xiaomimimo.com)، [z.ai/GLM](https://z.ai)، [Kimi/Moonshot](https://platform.moonshot.ai)، [MiniMax](https://www.minimax.io)، [Hugging Face](https://huggingface.co)، OpenAI، یا اپنا حسب ضرورت اینڈ پوائنٹ (endpoint) استعمال کریں۔ ماڈل تبدیل کرنے کے لیے صرف `hermes model` استعمال کریں — کسی کوڈ کو تبدیل کرنے کی ضرورت نہیں، کوئی پابندی نہیں۔
|
||||
|
||||
<table>
|
||||
<tr><td><b>حقیقی ٹرمینل انٹرفیس</b></td><td>مکمل TUI جس میں ملٹی لائن ایڈیٹنگ، سلیش-کمانڈ آٹو کمپلیٹ، بات چیت کی ہسٹری، انٹرپٹ اور ری ڈائریکٹ، اور سٹریمنگ ٹول آؤٹ پٹ شامل ہے۔</td></tr>
|
||||
<tr><td><b>یہ وہاں موجود ہے جہاں آپ ہیں</b></td><td>ٹیلی گرام، ڈسکارڈ (Discord)، سلیک (Slack)، واٹس ایپ (WhatsApp)، سگنل (Signal)، اور CLI — سب ایک ہی گیٹ وے پروسیس سے کام کرتے ہیں۔ وائس میمو (Voice memo) ٹرانسکرپشن، کراس پلیٹ فارم بات چیت کا تسلسل۔</td></tr>
|
||||
<tr><td><b>سیکھنے کا ایک مکمل عمل</b></td><td>ایجنٹ کی اپنی ترتیب دی گئی میموری، جس میں وہ خود کو وقتاً فوقتاً یاد دہانی کرواتا ہے۔ پیچیدہ کاموں کے بعد خود کار طریقے سے مہارت (skill) کی تخلیق۔ استعمال کے دوران مہارتوں میں بہتری۔ LLM سمرائزیشن کے ساتھ FTS5 سیشن سرچ تاکہ پرانے سیشنز کی یاددہانی کی جا سکے۔ <a href="https://github.com/plastic-labs/honcho">Honcho</a> کے ذریعے صارف کی ماڈلنگ۔ <a href="https://agentskills.io">agentskills.io</a> اوپن سٹینڈرڈ کے ساتھ مکمل مطابقت۔</td></tr>
|
||||
<tr><td><b>شیڈول کی گئی خودکار کارروائیاں</b></td><td>بلٹ ان (Built-in) کرون (cron) شیڈیولر جو کسی بھی پلیٹ فارم پر ڈیلیوری کے لیے استعمال ہو سکتا ہے۔ روزانہ کی رپورٹس، رات کے بیک اپس، ہفتہ وار آڈٹس — یہ سب کچھ قدرتی زبان (natural language) میں اور بغیر کسی نگرانی کے کام کرتا ہے۔</td></tr>
|
||||
<tr><td><b>کام کی تقسیم اور متوازی عمل</b></td><td>متوازی (parallel) کاموں کے لیے الگ سے ذیلی ایجنٹس (subagents) بنائیں۔ پائتھون (Python) سکرپٹس لکھیں جو RPC کے ذریعے ٹولز کو استعمال کریں، تاکہ کئی مراحل پر مشتمل کاموں کو بغیر کسی سیاق و سباق (context) کے خرچ کے، ایک ہی باری میں انجام دیا جا سکے۔</td></tr>
|
||||
<tr><td><b>کہیں بھی چلائیں، صرف اپنے لیپ ٹاپ پر نہیں</b></td><td>چھ (Six) ٹرمینل بیک اینڈز — لوکل، Docker، SSH، Singularity، Modal، اور Daytona۔ ڈیٹونا (Daytona) اور موڈل (Modal) سرور لیس (serverless) فعالیت پیش کرتے ہیں — جب آپ کا ایجنٹ فارغ ہوتا ہے تو اس کا ماحول سلیپ (hibernate) ہو جاتا ہے اور ضرورت پڑنے پر خود بخود جاگ جاتا ہے، جس کی وجہ سے سیشنز کے درمیان لاگت تقریباً صفر رہتی ہے۔ اسے $5 والے VPS یا GPU کلسٹر پر چلائیں۔</td></tr>
|
||||
<tr><td><b>تحقیق کے لیے تیار</b></td><td>بیچ (Batch) ٹریجیکٹری (trajectory) جنریشن، اگلی نسل کے ٹول کالنگ ماڈلز کی تربیت کے لیے ٹریجیکٹری کمپریشن۔</td></tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## فوری انسٹالیشن (Quick Install)
|
||||
|
||||
### لینکس (Linux)، میک او ایس (macOS)، ڈبلیو ایس ایل ٹو (WSL2)، ٹرمکس (Termux)
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
### ونڈوز (نیٹو، پاور شیل)
|
||||
|
||||
> **توجہ فرمائیں:** مقامی ونڈوز (Native Windows) پر ہرمیس بغیر WSL کے چلتا ہے — CLI، گیٹ وے، TUI، اور ٹولز سب مقامی طور پر کام کرتے ہیں۔ اگر آپ WSL2 استعمال کرنا پسند کرتے ہیں، تو اوپر دی گئی لینکس/میک او ایس کی کمانڈ وہاں بھی کام کرے گی۔ کوئی مسئلہ نظر آیا؟ براہ کرم [مسائل (issues) درج کریں](https://github.com/NousResearch/hermes-agent/issues)۔
|
||||
|
||||
اسے پاور شیل (PowerShell) میں چلائیں:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```powershell
|
||||
iex (irm https://hermes-agent.nousresearch.com/install.ps1)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
انسٹالر سب کچھ خود سنبھالتا ہے: uv، Python 3.11، Node.js، ripgrep، ffmpeg، **اور ایک پورٹ ایبل (portable) گٹ بیش (Git Bash)** (یعنی MinGit، جو `%LOCALAPPDATA%\hermes\git` میں ان پیک ہوتا ہے — اس کے لیے ایڈمن کی اجازت درکار نہیں، اور یہ سسٹم کے کسی بھی گٹ انسٹال سے بالکل الگ ہے)۔ ہرمیس اس بنڈل شدہ گٹ بیش کو شیل کمانڈز چلانے کے لیے استعمال کرتا ہے۔
|
||||
|
||||
اگر آپ کے پاس پہلے سے گٹ (Git) انسٹال ہے، تو انسٹالر اسے شناخت کر لیتا ہے اور اسے ہی استعمال کرتا ہے۔ بصورت دیگر آپ کو صرف ~45MB کے MinGit ڈاؤنلوڈ کی ضرورت ہوگی — یہ آپ کے سسٹم کے گٹ پر کوئی اثر نہیں ڈالے گا۔
|
||||
|
||||
> **اینڈرائیڈ (Android) / ٹرمکس (Termux):** ٹیسٹ کیا گیا مینوئل طریقہ [Termux گائیڈ](https://hermes-agent.nousresearch.com/docs/getting-started/termux) میں موجود ہے۔ ٹرمکس پر ہرمیس ایک مخصوص `.[termux]` ایکسٹرا انسٹال کرتا ہے کیونکہ مکمل `.[all]` ایکسٹرا میں ایسی وائس ڈیپینڈینسیز شامل ہیں جو اینڈرائیڈ کے ساتھ مطابقت نہیں رکھتیں۔
|
||||
>
|
||||
> **ونڈوز (Windows):** مقامی ونڈوز کی مکمل سپورٹ موجود ہے — اوپر دی گئی پاور شیل کی کمانڈ سب کچھ انسٹال کر دیتی ہے۔ اگر آپ WSL2 استعمال کرنا چاہتے ہیں، تو لینکس کی کمانڈ وہاں کام کرتی ہے۔ مقامی ونڈوز میں انسٹالیشن `%LOCALAPPDATA%\hermes` میں ہوتی ہے؛ جبکہ WSL2 میں لینکس کی طرح `~/.hermes` میں ہوتی ہے۔ ہرمیس کا وہ واحد فیچر جسے فی الحال خاص طور پر WSL2 کی ضرورت ہے وہ براؤزر پر مبنی ڈیش بورڈ چیٹ پین ہے (یہ POSIX PTY استعمال کرتا ہے — کلاسک CLI اور گیٹ وے دونوں مقامی طور پر چلتے ہیں)۔
|
||||
|
||||
انسٹالیشن کے بعد:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```bash
|
||||
source ~/.bashrc # شیل کو ری لوڈ کریں (یا: source ~/.zshrc)
|
||||
hermes # بات چیت شروع کریں!
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## آغاز کریں (Getting Started)
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```bash
|
||||
hermes # انٹرایکٹو CLI — بات چیت شروع کریں
|
||||
hermes model # اپنا LLM پرووائیڈر اور ماڈل منتخب کریں
|
||||
hermes tools # کنفیگر کریں کہ کون سے ٹولز ایکٹو ہیں
|
||||
hermes config set # انفرادی کنفگ (config) ویلیوز سیٹ کریں
|
||||
hermes gateway # میسجنگ گیٹ وے شروع کریں (ٹیلی گرام، ڈسکارڈ، وغیرہ)
|
||||
hermes setup # مکمل سیٹ اپ وزرڈ چلائیں (یہ سب کچھ ایک ساتھ کنفیگر کر دے گا)
|
||||
hermes claw migrate # OpenClaw سے مائیگریٹ کریں (اگر آپ OpenClaw سے آ رہے ہیں)
|
||||
hermes update # لیٹسٹ ورژن پر اپ ڈیٹ کریں
|
||||
hermes doctor # کسی بھی مسئلے کی تشخیص کریں
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
📖 **[مکمل دستاویزات →](https://hermes-agent.nousresearch.com/docs/)**
|
||||
|
||||
---
|
||||
|
||||
## API-کیز اکٹھی کرنے سے بچیں — Nous Portal
|
||||
|
||||
ہرمیس آپ کے پسندیدہ پرووائیڈر کے ساتھ کام کرتا ہے — یہ چیز تبدیل نہیں ہو رہی۔ لیکن اگر آپ ماڈل، ویب سرچ، امیج جنریشن، TTS، اور کلاؤڈ براؤزر کے لیے پانچ الگ الگ API کیز جمع نہیں کرنا چاہتے، تو **[Nous Portal](https://portal.nousresearch.com)** ان سب کو ایک ہی سبسکرپشن کے تحت کور کرتا ہے:
|
||||
|
||||
- **300+ ماڈلز** — ان میں سے کوئی بھی ماڈل `/model <name>` کے ذریعے منتخب کریں
|
||||
- **ٹول گیٹ وے (Tool Gateway)** — ویب سرچ (Firecrawl)، امیج جنریشن (FAL)، ٹیکسٹ ٹو سپیچ (OpenAI)، کلاؤڈ براؤزر (Browser Use)، یہ سب آپ کی سبسکرپشن کے ذریعے چلتے ہیں۔ کسی اضافی اکاؤنٹ کی ضرورت نہیں۔
|
||||
|
||||
نئی انسٹالیشن کے بعد بس ایک کمانڈ کی ضرورت ہے:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```bash
|
||||
hermes setup --portal
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
یہ آپ کو OAuth کے ذریعے لاگ ان کرواتا ہے، Nous کو آپ کا پرووائیڈر مقرر کرتا ہے، اور ٹول گیٹ وے کو آن کر دیتا ہے۔ `hermes portal info` کمانڈ استعمال کر کے آپ کسی بھی وقت چیک کر سکتے ہیں کہ کون کون سی سروسز منسلک ہیں۔ مکمل تفصیلات [Tool Gateway دستاویزات کے صفحے](https://hermes-agent.nousresearch.com/docs/user-guide/features/tool-gateway) پر موجود ہیں۔
|
||||
|
||||
آپ اب بھی کسی بھی ٹول کے لیے اپنی مرضی کی API کیز استعمال کر سکتے ہیں — گیٹ وے ہر سروس کے لیے الگ الگ کام کرتا ہے، ایسا نہیں کہ یا تو سب کچھ استعمال کریں یا کچھ بھی نہیں۔
|
||||
|
||||
---
|
||||
|
||||
## CLI بمقابلہ میسجنگ فوری حوالہ
|
||||
|
||||
ہرمیس کے دو بنیادی انٹر فیس ہیں: آپ ٹرمینل UI کو `hermes` کے ساتھ شروع کریں، یا گیٹ وے چلا کر اس کے ساتھ ٹیلی گرام، ڈسکارڈ، سلیک، واٹس ایپ، سگنل، یا ای میل کے ذریعے بات کریں۔ جب آپ کسی بات چیت میں ہوتے ہیں، تو بہت سی سلیش (slash) کمانڈز دونوں انٹرفیسز میں ایک جیسی ہوتی ہیں۔
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
| کارروائی (Action) | سی ایل آئی (CLI) | میسجنگ پلیٹ فارمز (Messaging platforms) |
|
||||
| --------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| بات چیت شروع کریں | `hermes` | `hermes gateway setup` اور `hermes gateway start` چلائیں، پھر بوٹ کو میسج بھیجیں |
|
||||
| نئی بات چیت شروع کریں | `/new` یا `/reset` | `/new` یا `/reset` |
|
||||
| ماڈل تبدیل کریں | `/model [provider:model]` | `/model [provider:model]` |
|
||||
| پرسنلٹی (Personality) سیٹ کریں | `/personality [name]` | `/personality [name]` |
|
||||
| پچھلی باری کو دوبارہ یا منسوخ (undo) کریں | `/retry`، `/undo` | `/retry`، `/undo` |
|
||||
| کانٹیکسٹ (context) کمپریس کریں / استعمال چیک کریں | `/compress`، `/usage`، `/insights [--days N]` | `/compress`، `/usage`، `/insights [days]` |
|
||||
| مہارتیں (Skills) براؤز کریں | `/skills` یا `/<skill-name>` | `/<skill-name>` |
|
||||
| موجودہ کام کو روکیں | `Ctrl+C` دبائیں یا نیا میسج بھیجیں | `/stop` یا نیا میسج بھیجیں |
|
||||
| پلیٹ فارم کے لحاظ سے سٹیٹس | `/platforms` | `/status`، `/sethome` |
|
||||
|
||||
</div>
|
||||
|
||||
مکمل کمانڈ لسٹ کے لیے، [CLI گائیڈ](https://hermes-agent.nousresearch.com/docs/user-guide/cli) اور [میسجنگ گیٹ وے گائیڈ](https://hermes-agent.nousresearch.com/docs/user-guide/messaging) دیکھیں۔
|
||||
|
||||
---
|
||||
|
||||
## دستاویزات (Documentation)
|
||||
|
||||
تمام دستاویزات **[hermes-agent.nousresearch.com/docs](https://hermes-agent.nousresearch.com/docs/)** پر موجود ہیں:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
| سیکشن (Section) | تفصیل (What's Covered) |
|
||||
| --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| [فوری آغاز (Quickstart)](https://hermes-agent.nousresearch.com/docs/getting-started/quickstart) | انسٹالیشن → سیٹ اپ → 2 منٹ میں پہلی بات چیت شروع کریں |
|
||||
| [CLI کا استعمال](https://hermes-agent.nousresearch.com/docs/user-guide/cli) | کمانڈز، کی بائنڈنگز (keybindings)، پرسنلٹیز (personalities)، سیشنز |
|
||||
| [کنفیگریشن (Configuration)](https://hermes-agent.nousresearch.com/docs/user-guide/configuration) | کنفگ فائل، پرووائیڈرز، ماڈلز، اور تمام آپشنز |
|
||||
| [میسجنگ گیٹ وے](https://hermes-agent.nousresearch.com/docs/user-guide/messaging) | ٹیلی گرام، ڈسکارڈ، سلیک، واٹس ایپ، سگنل، ہوم اسسٹنٹ |
|
||||
| [سیکیورٹی (Security)](https://hermes-agent.nousresearch.com/docs/user-guide/security) | کمانڈ کی منظوری، DM پیئرنگ (pairing)، کنٹینر آئسولیشن |
|
||||
| [ٹولز اور ٹول سیٹس](https://hermes-agent.nousresearch.com/docs/user-guide/features/tools) | 40 سے زائد ٹولز، ٹول سیٹ سسٹم، ٹرمینل بیک اینڈز |
|
||||
| [مہارتوں کا سسٹم (Skills System)](https://hermes-agent.nousresearch.com/docs/user-guide/features/skills)| پروسیجرل (Procedural) میموری، سکلز ہب، نئی مہارتیں بنانا |
|
||||
| [میموری (Memory)](https://hermes-agent.nousresearch.com/docs/user-guide/features/memory) | مستقل میموری، یوزر پروفائلز، بہترین طریقہ کار |
|
||||
| [MCP انضمام (Integration)](https://hermes-agent.nousresearch.com/docs/user-guide/features/mcp) | صلاحیتوں کو بڑھانے کے لیے کسی بھی MCP سرور کو جوڑیں |
|
||||
| [کرون (Cron) شیڈیولنگ](https://hermes-agent.nousresearch.com/docs/user-guide/features/cron) | پلیٹ فارم ڈیلیوری کے ساتھ شیڈول کیے گئے کام |
|
||||
| [کانٹیکسٹ (Context) فائلز](https://hermes-agent.nousresearch.com/docs/user-guide/features/context-files)| پروجیکٹ کا سیاق و سباق (context) جو ہر بات چیت پر اثر انداز ہوتا ہے |
|
||||
| [آرکیٹیکچر (Architecture)](https://hermes-agent.nousresearch.com/docs/developer-guide/architecture) | پروجیکٹ کا ڈھانچہ، ایجنٹ لوپ، اہم کلاسز |
|
||||
| [تعاون (Contributing)](https://hermes-agent.nousresearch.com/docs/developer-guide/contributing) | ڈیویلپمنٹ سیٹ اپ، PR کا طریقہ کار، کوڈنگ کا انداز |
|
||||
| [CLI حوالہ جات (Reference)](https://hermes-agent.nousresearch.com/docs/reference/cli-commands) | تمام کمانڈز اور فلیگز (flags) |
|
||||
| [انوائرمنٹ ویری ایبلز](https://hermes-agent.nousresearch.com/docs/reference/environment-variables) | مکمل انوائرمنٹ ویری ایبل حوالہ جات |
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## OpenClaw سے منتقلی
|
||||
|
||||
اگر آپ OpenClaw سے منتقل ہو رہے ہیں، تو ہرمیس آپ کی سیٹنگز، یادیں (memories)، مہارتیں (skills)، اور API کیز کو خود بخود امپورٹ کر سکتا ہے۔
|
||||
|
||||
**پہلی بار سیٹ اپ کے دوران:** سیٹ اپ وزرڈ (`hermes setup`) خود بخود `~/.openclaw` کو پہچان لیتا ہے اور کنفیگریشن شروع ہونے سے پہلے مائیگریٹ (migrate) کرنے کا آپشن دیتا ہے۔
|
||||
|
||||
**انسٹالیشن کے بعد کسی بھی وقت:**
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```bash
|
||||
hermes claw migrate # انٹرایکٹو مائیگریشن (مکمل پری سیٹ)
|
||||
hermes claw migrate --dry-run # جائزہ لیں کہ کیا کیا مائیگریٹ ہوگا
|
||||
hermes claw migrate --preset user-data # حساس معلومات (secrets) کے بغیر مائیگریٹ کریں
|
||||
hermes claw migrate --overwrite # موجودہ متصادم فائلوں کو اوور رائٹ کریں
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
جو چیزیں امپورٹ ہوتی ہیں:
|
||||
|
||||
- **SOUL.md** — پرسونا (persona) فائل
|
||||
- **میموریز (Memories)** — MEMORY.md اور USER.md کی اندراجات
|
||||
- **مہارتیں (Skills)** — صارف کی بنائی گئی مہارتیں → `~/.hermes/skills/openclaw-imports/`
|
||||
- **کمانڈ الاؤ لسٹ (allowlist)** — منظوری کے پیٹرنز (approval patterns)
|
||||
- **میسجنگ سیٹنگز** — پلیٹ فارم کنفیگریشنز، اجازت یافتہ صارفین، ورکنگ ڈائریکٹری
|
||||
- **API کیز** — الاؤ لسٹ شدہ حساس معلومات (ٹیلی گرام، OpenRouter، OpenAI، Anthropic، ElevenLabs)
|
||||
- **TTS اثاثے** — ورک اسپیس کی آڈیو فائلیں
|
||||
- **ورک اسپیس کی ہدایات** — AGENTS.md (`--workspace-target` کے ساتھ)
|
||||
|
||||
تمام آپشنز دیکھنے کے لیے `hermes claw migrate --help` استعمال کریں، یا انٹرایکٹو ایجنٹ کی مدد سے مائیگریٹ کرنے کے لیے `openclaw-migration` سکل کا استعمال کریں (جس میں ڈرائی رن (dry-run) پریویوز شامل ہیں)۔
|
||||
|
||||
---
|
||||
|
||||
## تعاون کریں (Contributing)
|
||||
|
||||
ہم آپ کے تعاون کا خیرمقدم کرتے ہیں! ڈیویلپمنٹ سیٹ اپ، کوڈ کے انداز اور PR کے طریقہ کار کے لیے براہ کرم ہماری [Contributing گائیڈ](https://hermes-agent.nousresearch.com/docs/developer-guide/contributing) دیکھیں۔
|
||||
|
||||
معاونین (contributors) کے لیے فوری آغاز — کلون (clone) کریں اور `setup-hermes.sh` چلائیں:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NousResearch/hermes-agent.git
|
||||
cd hermes-agent
|
||||
./setup-hermes.sh # uv کو انسٹال کرتا ہے، venv بناتا ہے، .[all] کو انسٹال کرتا ہے، اور ~/.local/bin/hermes کا سیم لنک (symlink) بناتا ہے
|
||||
./hermes # خود بخود venv کی شناخت کرتا ہے، پہلے `source` کرنے کی ضرورت نہیں
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
مینوئل طریقہ (اوپر والے طریقے کے مساوی):
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
uv venv .venv --python 3.11
|
||||
source .venv/bin/activate
|
||||
uv pip install -e ".[all,dev]"
|
||||
scripts/run_tests.sh
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## کمیونٹی (Community)
|
||||
|
||||
- 💬 [ڈسکارڈ (Discord)](https://discord.gg/NousResearch)
|
||||
- 📚 [سکلز ہب (Skills Hub)](https://agentskills.io)
|
||||
- 🐛 [مسائل (Issues)](https://github.com/NousResearch/hermes-agent/issues)
|
||||
- 🔌 [computer-use-linux](https://github.com/avifenesh/computer-use-linux) — ہرمیس اور دیگر MCP ہوسٹس کے لیے لینکس (Linux) ڈیسک ٹاپ کنٹرول MCP سرور، جس میں AT-SPI ایکسیسیبلٹی ٹریز، Wayland/X11 ان پٹ، سکرین شاٹس، اور کمپوزیٹر ونڈو ٹارگیٹنگ شامل ہے۔
|
||||
- 🔌 [HermesClaw](https://github.com/AaronWong1999/hermesclaw) — کمیونٹی وی چیٹ (WeChat) برج: ہرمیس ایجنٹ اور OpenClaw کو ایک ہی وی چیٹ اکاؤنٹ پر چلائیں۔
|
||||
|
||||
---
|
||||
|
||||
## لائسنس (License)
|
||||
|
||||
MIT — تفصیلات کے لیے [LICENSE](LICENSE) دیکھیں۔
|
||||
|
||||
[نوس ریسرچ (Nous Research)](https://nousresearch.com) کی جانب سے تیار کردہ۔
|
||||
|
||||
</div>
|
||||
@@ -10,6 +10,7 @@
|
||||
<a href="https://github.com/NousResearch/hermes-agent/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License: MIT"></a>
|
||||
<a href="https://nousresearch.com"><img src="https://img.shields.io/badge/Built%20by-Nous%20Research-blueviolet?style=for-the-badge" alt="Built by Nous Research"></a>
|
||||
<a href="README.md"><img src="https://img.shields.io/badge/Lang-English-lightgrey?style=for-the-badge" alt="English"></a>
|
||||
<a href="README.ur-pk.md"><img src="https://img.shields.io/badge/Lang-اردو-green?style=for-the-badge" alt="اردو"></a>
|
||||
</p>
|
||||
|
||||
**由 [Nous Research](https://nousresearch.com) 构建的自进化 AI 代理。** 它是唯一内置学习闭环的智能代理——从经验中创建技能,在使用中改进技能,主动持久化知识,搜索过往对话,并在跨会话中逐步构建对你的深度理解。可以在 $5 的 VPS 上运行,也可以在 GPU 集群上运行,或者使用几乎零成本的 Serverless 基础设施。它不绑定你的笔记本——你可以在 Telegram 上与它对话,而它在云端 VM 上工作。
|
||||
|
||||
127
acp_adapter/provenance.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""Derive ACP session-provenance metadata from the existing compression chain.
|
||||
|
||||
This is an additive Hermes extension surfaced under ACP ``_meta.hermes`` so
|
||||
existing ACP clients ignore it. It carries no new persisted state: everything
|
||||
is derived on demand from the ``sessions`` table (``parent_session_id`` /
|
||||
``end_reason``), which already models compression-continuation chains.
|
||||
|
||||
The ACP/editor ``session_id`` stays the stable public handle. When context
|
||||
compression rotates the internal Hermes head, ``build_session_provenance`` lets
|
||||
a client see the previous/current internal ids and the lineage root without
|
||||
parsing status text, guessing from token drops, or reading ``state.db``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# Bound defensive walks; compression chains this deep are pathological.
|
||||
_MAX_WALK = 100
|
||||
|
||||
|
||||
def build_session_provenance(
|
||||
db: Any,
|
||||
acp_session_id: str,
|
||||
current_hermes_session_id: str,
|
||||
*,
|
||||
previous_hermes_session_id: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Build ``_meta.hermes.sessionProvenance`` for an ACP session.
|
||||
|
||||
Args:
|
||||
db: A ``SessionDB`` (must expose ``get_session``).
|
||||
acp_session_id: The stable ACP/editor-facing session handle.
|
||||
current_hermes_session_id: The live internal Hermes DB session id
|
||||
(``state.agent.session_id``).
|
||||
previous_hermes_session_id: The internal id from before the most recent
|
||||
turn, when known. Supplied by ``prompt()`` to flag a rotation.
|
||||
|
||||
Returns:
|
||||
A dict suitable for ``{"hermes": {"sessionProvenance": <dict>}}`` under
|
||||
ACP ``_meta``, or ``None`` if the session can't be read.
|
||||
"""
|
||||
try:
|
||||
row = db.get_session(current_hermes_session_id)
|
||||
except Exception:
|
||||
return None
|
||||
if not row:
|
||||
return None
|
||||
|
||||
parent_id = row.get("parent_session_id")
|
||||
end_reason = row.get("end_reason")
|
||||
|
||||
# Walk parents to the lineage root and count compression depth. Only
|
||||
# compression-split parents (parent.end_reason == 'compression') count
|
||||
# toward depth — delegate/branch children share the parent_session_id
|
||||
# column but are not compaction boundaries.
|
||||
root_id = current_hermes_session_id
|
||||
compression_depth = 0
|
||||
cursor_parent = parent_id
|
||||
seen = {current_hermes_session_id}
|
||||
for _ in range(_MAX_WALK):
|
||||
if not cursor_parent or cursor_parent in seen:
|
||||
break
|
||||
seen.add(cursor_parent)
|
||||
try:
|
||||
prow = db.get_session(cursor_parent)
|
||||
except Exception:
|
||||
prow = None
|
||||
if not prow:
|
||||
break
|
||||
root_id = cursor_parent
|
||||
if prow.get("end_reason") == "compression":
|
||||
compression_depth += 1
|
||||
cursor_parent = prow.get("parent_session_id")
|
||||
|
||||
# A session is a compression continuation when its parent was ended with
|
||||
# end_reason='compression'. Determine that from the immediate parent.
|
||||
is_continuation = False
|
||||
if parent_id:
|
||||
try:
|
||||
immediate_parent = db.get_session(parent_id)
|
||||
except Exception:
|
||||
immediate_parent = None
|
||||
if immediate_parent and immediate_parent.get("end_reason") == "compression":
|
||||
is_continuation = True
|
||||
|
||||
rotated = bool(
|
||||
previous_hermes_session_id
|
||||
and previous_hermes_session_id != current_hermes_session_id
|
||||
)
|
||||
|
||||
provenance: Dict[str, Any] = {
|
||||
"acpSessionId": acp_session_id,
|
||||
"currentHermesSessionId": current_hermes_session_id,
|
||||
"rootHermesSessionId": root_id,
|
||||
"parentHermesSessionId": parent_id,
|
||||
"sessionKind": "continuation" if is_continuation else "root",
|
||||
"compressionDepth": compression_depth,
|
||||
}
|
||||
if previous_hermes_session_id:
|
||||
provenance["previousHermesSessionId"] = previous_hermes_session_id
|
||||
if rotated:
|
||||
# The head moved during the last turn. The only mechanism that rotates
|
||||
# the internal id mid-turn is compression-driven session splitting.
|
||||
provenance["reason"] = "compression"
|
||||
provenance["creatorKind"] = "compression"
|
||||
|
||||
return provenance
|
||||
|
||||
|
||||
def session_provenance_meta(
|
||||
db: Any,
|
||||
acp_session_id: str,
|
||||
current_hermes_session_id: str,
|
||||
*,
|
||||
previous_hermes_session_id: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Return a ready ``_meta`` payload: ``{"hermes": {"sessionProvenance": ...}}``."""
|
||||
prov = build_session_provenance(
|
||||
db,
|
||||
acp_session_id,
|
||||
current_hermes_session_id,
|
||||
previous_hermes_session_id=previous_hermes_session_id,
|
||||
)
|
||||
if prov is None:
|
||||
return None
|
||||
return {"hermes": {"sessionProvenance": prov}}
|
||||
@@ -71,6 +71,7 @@ from acp_adapter.events import (
|
||||
make_tool_progress_cb,
|
||||
)
|
||||
from acp_adapter.permissions import make_approval_callback
|
||||
from acp_adapter.provenance import session_provenance_meta
|
||||
from acp_adapter.session import SessionManager, SessionState, _expand_acp_enabled_toolsets
|
||||
from acp_adapter.tools import build_tool_complete, build_tool_start
|
||||
|
||||
@@ -709,8 +710,39 @@ class HermesACPAgent(acp.Agent):
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
async def _send_session_info_update(self, session_id: str) -> None:
|
||||
"""Send ACP native session metadata after Hermes changes it."""
|
||||
def _provenance_meta(
|
||||
self,
|
||||
acp_session_id: str,
|
||||
current_hermes_session_id: str,
|
||||
previous_hermes_session_id: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
"""Best-effort ``_meta.hermes.sessionProvenance`` for an ACP session."""
|
||||
try:
|
||||
return session_provenance_meta(
|
||||
self.session_manager._get_db(),
|
||||
acp_session_id,
|
||||
current_hermes_session_id,
|
||||
previous_hermes_session_id=previous_hermes_session_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"Could not build ACP session provenance for %s", acp_session_id, exc_info=True
|
||||
)
|
||||
return None
|
||||
|
||||
async def _send_session_info_update(
|
||||
self,
|
||||
session_id: str,
|
||||
*,
|
||||
current_hermes_session_id: Optional[str] = None,
|
||||
previous_hermes_session_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Send ACP native session metadata after Hermes changes it.
|
||||
|
||||
When the internal Hermes head rotated (e.g. compression-driven session
|
||||
split during a turn), pass ``previous_hermes_session_id`` so the
|
||||
attached ``_meta.hermes.sessionProvenance`` flags the rotation reason.
|
||||
"""
|
||||
if not self._conn:
|
||||
return
|
||||
try:
|
||||
@@ -727,10 +759,16 @@ class HermesACPAgent(acp.Agent):
|
||||
# the updated_at since we're emitting this notification precisely
|
||||
# because the title was just refreshed.
|
||||
updated_at = datetime.now(timezone.utc).isoformat()
|
||||
meta = self._provenance_meta(
|
||||
session_id,
|
||||
current_hermes_session_id or session_id,
|
||||
previous_hermes_session_id,
|
||||
)
|
||||
update = SessionInfoUpdate(
|
||||
session_update="session_info_update",
|
||||
title=title if isinstance(title, str) and title.strip() else None,
|
||||
updated_at=updated_at,
|
||||
field_meta=meta,
|
||||
)
|
||||
try:
|
||||
await self._conn.session_update(
|
||||
@@ -1081,6 +1119,9 @@ class HermesACPAgent(acp.Agent):
|
||||
session_id=state.session_id,
|
||||
models=self._build_model_state(state),
|
||||
modes=self._session_modes(state),
|
||||
field_meta=self._provenance_meta(
|
||||
state.session_id, getattr(state.agent, "session_id", state.session_id)
|
||||
),
|
||||
)
|
||||
|
||||
async def load_session(
|
||||
@@ -1125,6 +1166,9 @@ class HermesACPAgent(acp.Agent):
|
||||
return LoadSessionResponse(
|
||||
models=self._build_model_state(state),
|
||||
modes=self._session_modes(state),
|
||||
field_meta=self._provenance_meta(
|
||||
session_id, getattr(state.agent, "session_id", session_id)
|
||||
),
|
||||
)
|
||||
|
||||
async def resume_session(
|
||||
@@ -1157,6 +1201,9 @@ class HermesACPAgent(acp.Agent):
|
||||
return ResumeSessionResponse(
|
||||
models=self._build_model_state(state),
|
||||
modes=self._session_modes(state),
|
||||
field_meta=self._provenance_meta(
|
||||
state.session_id, getattr(state.agent, "session_id", state.session_id)
|
||||
),
|
||||
)
|
||||
|
||||
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
||||
@@ -1494,6 +1541,11 @@ class HermesACPAgent(acp.Agent):
|
||||
logger.debug("Could not clear ACP session context", exc_info=True)
|
||||
|
||||
try:
|
||||
# Snapshot the internal Hermes DB session id before the turn so we
|
||||
# can detect a compression-driven session rotation afterwards. The
|
||||
# ACP `session_id` stays the stable client handle; agent.session_id
|
||||
# is the live internal head that compression may rotate.
|
||||
pre_turn_hermes_id = getattr(state.agent, "session_id", None)
|
||||
# Wrap the executor call in a fresh copy of the current context so
|
||||
# concurrent ACP sessions on the shared ThreadPoolExecutor don't
|
||||
# stomp on each other's ContextVar writes (HERMES_SESSION_KEY in
|
||||
@@ -1512,8 +1564,41 @@ class HermesACPAgent(acp.Agent):
|
||||
# Persist updated history so sessions survive process restarts.
|
||||
self.session_manager.save_session(session_id)
|
||||
|
||||
# Detect a compression-driven internal session rotation. If the agent's
|
||||
# DB head moved during the turn, emit a session_info_update carrying
|
||||
# _meta.hermes.sessionProvenance so ACP clients can render the boundary
|
||||
# and keep old/new ids in lineage. The ACP session_id is unchanged.
|
||||
post_turn_hermes_id = getattr(state.agent, "session_id", None)
|
||||
if (
|
||||
conn
|
||||
and post_turn_hermes_id
|
||||
and pre_turn_hermes_id
|
||||
and post_turn_hermes_id != pre_turn_hermes_id
|
||||
):
|
||||
try:
|
||||
await self._send_session_info_update(
|
||||
session_id,
|
||||
current_hermes_session_id=post_turn_hermes_id,
|
||||
previous_hermes_session_id=pre_turn_hermes_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"Could not emit ACP provenance update after rotation for %s",
|
||||
session_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
final_response = result.get("final_response", "")
|
||||
if final_response:
|
||||
cancelled = bool(state.cancel_event and state.cancel_event.is_set())
|
||||
interrupted = bool(result.get("interrupted")) or cancelled
|
||||
# Hermes' local "waiting for model response" interrupt status is metadata,
|
||||
# not assistant prose — clients get cancellation from stop_reason instead.
|
||||
from agent.conversation_loop import INTERRUPT_WAITING_FOR_MODEL_PREFIX
|
||||
|
||||
suppress_interrupt_response = interrupted and final_response.startswith(
|
||||
INTERRUPT_WAITING_FOR_MODEL_PREFIX
|
||||
)
|
||||
if final_response and not suppress_interrupt_response:
|
||||
try:
|
||||
from agent.title_generator import maybe_auto_title
|
||||
|
||||
@@ -1534,7 +1619,12 @@ class HermesACPAgent(acp.Agent):
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Failed to auto-title ACP session %s", session_id, exc_info=True)
|
||||
if final_response and conn and (not streamed_message or result.get("response_transformed")):
|
||||
if (
|
||||
final_response
|
||||
and conn
|
||||
and not suppress_interrupt_response
|
||||
and (not streamed_message or result.get("response_transformed"))
|
||||
):
|
||||
# Deliver the final response when streaming did not already send it,
|
||||
# or when a plugin hook transformed the response after streaming
|
||||
# finished (e.g. transform_llm_output) — otherwise the appended /
|
||||
@@ -1576,7 +1666,7 @@ class HermesACPAgent(acp.Agent):
|
||||
|
||||
await self._send_usage_update(state)
|
||||
|
||||
stop_reason = "cancelled" if state.cancel_event and state.cancel_event.is_set() else "end_turn"
|
||||
stop_reason = "cancelled" if cancelled else "end_turn"
|
||||
return PromptResponse(stop_reason=stop_reason, usage=usage)
|
||||
|
||||
# ---- Slash commands (headless) -------------------------------------------
|
||||
|
||||
@@ -242,6 +242,17 @@ def nous_credits_lines(*, markdown: bool = False, timeout: float = 10.0) -> list
|
||||
renders from that fixture instead of the real portal (so the block + gauge are
|
||||
testable without a live account). Throwaway scaffolding.
|
||||
"""
|
||||
snapshot = _fetch_nous_credits_snapshot(timeout=timeout)
|
||||
return render_account_usage_lines(snapshot, markdown=markdown)
|
||||
|
||||
|
||||
def _fetch_nous_credits_snapshot(timeout: float = 10.0) -> Optional[AccountUsageSnapshot]:
|
||||
"""Auth-gate + portal fetch + snapshot build for the Nous credits block.
|
||||
|
||||
Shared by ``nous_credits_lines`` (full block) and
|
||||
``nous_credits_compact_line`` (one-liner). Honors the
|
||||
HERMES_DEV_CREDITS_FIXTURE dev override. Fail-open → None.
|
||||
"""
|
||||
# Dev fixture short-circuit — render /usage from the injected state, no portal.
|
||||
try:
|
||||
from agent.credits_tracker import dev_fixture_credits_state
|
||||
@@ -250,17 +261,16 @@ def nous_credits_lines(*, markdown: bool = False, timeout: float = 10.0) -> list
|
||||
except Exception:
|
||||
fixture = None
|
||||
if fixture is not None:
|
||||
snapshot = _snapshot_from_credits_state(fixture)
|
||||
return render_account_usage_lines(snapshot, markdown=markdown)
|
||||
return _snapshot_from_credits_state(fixture)
|
||||
|
||||
try:
|
||||
from hermes_cli.auth import get_provider_auth_state
|
||||
|
||||
tok = (get_provider_auth_state("nous") or {}).get("access_token")
|
||||
if not (isinstance(tok, str) and tok.strip()):
|
||||
return []
|
||||
return None
|
||||
except Exception:
|
||||
return []
|
||||
return None
|
||||
try:
|
||||
import concurrent.futures
|
||||
|
||||
@@ -270,13 +280,36 @@ def nous_credits_lines(*, markdown: bool = False, timeout: float = 10.0) -> list
|
||||
account = pool.submit(
|
||||
get_nous_portal_account_info, force_fresh=True
|
||||
).result(timeout=timeout)
|
||||
snapshot = build_nous_credits_snapshot(account)
|
||||
return render_account_usage_lines(snapshot, markdown=markdown)
|
||||
return build_nous_credits_snapshot(account)
|
||||
except Exception:
|
||||
# Fail-open (caller shows nothing), but leave a breadcrumb so a dead
|
||||
# /usage credits block is diagnosable in agent.log without a dev flag.
|
||||
logger.debug("credits ▸ /usage portal fetch/render failed (fail-open)", exc_info=True)
|
||||
return []
|
||||
return None
|
||||
|
||||
|
||||
def nous_credits_compact_line(*, timeout: float = 10.0) -> Optional[str]:
|
||||
"""One-line Nous credits summary for the compact /usage view, or None.
|
||||
|
||||
Condenses the snapshot's own detail strings (stable, locally-built
|
||||
formats) into ``Nous credits (Plan): Total usable: $X · Renews: …``.
|
||||
Same gating/fail-open semantics as ``nous_credits_lines``.
|
||||
"""
|
||||
snap = _fetch_nous_credits_snapshot(timeout=timeout)
|
||||
if snap is None or not snap.available:
|
||||
return None
|
||||
picked = [
|
||||
d for d in snap.details
|
||||
if d.startswith(("Total usable:", "Renews:", "Status:"))
|
||||
]
|
||||
if not picked:
|
||||
picked = [d for d in snap.details if not d.startswith("Manage / top up:")][:2]
|
||||
if not picked:
|
||||
return None
|
||||
title = snap.title
|
||||
if snap.plan:
|
||||
title += f" ({snap.plan})"
|
||||
return f"{title}: " + " · ".join(picked)
|
||||
|
||||
|
||||
def _snapshot_from_credits_state(state) -> Optional[AccountUsageSnapshot]:
|
||||
|
||||
@@ -1620,6 +1620,12 @@ def init_agent(
|
||||
agent.session_cache_write_tokens = 0
|
||||
agent.session_reasoning_tokens = 0
|
||||
agent.session_estimated_cost_usd = 0.0
|
||||
# Provider-REPORTED cost only (e.g. OpenRouter usage.cost). None means
|
||||
# "nothing reported" — distinct from a real $0.00.
|
||||
agent.session_actual_cost_usd = None
|
||||
# Per-model session usage rows for /usage: {model: {calls, input, output,
|
||||
# cache_read, cache_write, cost_usd|None}}.
|
||||
agent.session_model_usage = {}
|
||||
agent.session_cost_status = "unknown"
|
||||
agent.session_cost_source = "none"
|
||||
|
||||
|
||||
@@ -1846,6 +1846,27 @@ def repair_tool_call(agent, tool_name: str) -> str | None:
|
||||
if not tool_name:
|
||||
return None
|
||||
|
||||
# VolcEngine api/plan workaround (issue #33007): the endpoint's
|
||||
# protocol-translation layer occasionally leaks raw XML attribute
|
||||
# fragments into tool_use.name, e.g.
|
||||
# `terminal" parameter="command" string="true`
|
||||
# `execute_code" parameter="code" string="true`
|
||||
# `session_search" parameter="session_id" string="true`
|
||||
# We trim at the first unambiguous XML/quote character so the rest
|
||||
# of the repair pipeline (lowercase / snake_case / fuzzy match)
|
||||
# can resolve the cleaned name to a real tool.
|
||||
#
|
||||
# Crucially we DO NOT split on whitespace: legitimate inputs like
|
||||
# "write file" must keep flowing through ``_norm`` -> ``write_file``
|
||||
# (covered by test_space_to_underscore in
|
||||
# tests/run_agent/test_repair_tool_call_name.py).
|
||||
for _xml_sep in ('"', "'", "<", ">"):
|
||||
_idx = tool_name.find(_xml_sep)
|
||||
if _idx > 0:
|
||||
tool_name = tool_name[:_idx]
|
||||
if not tool_name:
|
||||
return None
|
||||
|
||||
def _norm(s: str) -> str:
|
||||
return s.lower().replace("-", "_").replace(" ", "_")
|
||||
|
||||
|
||||
@@ -637,54 +637,6 @@ def _pool_runtime_base_url(entry: Any, fallback: str = "") -> str:
|
||||
# calls to the Codex Responses API so callers don't need any changes.
|
||||
|
||||
|
||||
def _convert_content_for_responses(content: Any) -> Any:
|
||||
"""Convert chat.completions content to Responses API format.
|
||||
|
||||
chat.completions uses:
|
||||
{"type": "text", "text": "..."}
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
|
||||
|
||||
Responses API uses:
|
||||
{"type": "input_text", "text": "..."}
|
||||
{"type": "input_image", "image_url": "data:image/png;base64,..."}
|
||||
|
||||
If content is a plain string, it's returned as-is (the Responses API
|
||||
accepts strings directly for text-only messages).
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if not isinstance(content, list):
|
||||
return str(content) if content else ""
|
||||
|
||||
converted: List[Dict[str, Any]] = []
|
||||
for part in content:
|
||||
if not isinstance(part, dict):
|
||||
continue
|
||||
ptype = part.get("type", "")
|
||||
if ptype == "text":
|
||||
converted.append({"type": "input_text", "text": part.get("text", "")})
|
||||
elif ptype == "image_url":
|
||||
# chat.completions nests the URL: {"image_url": {"url": "..."}}
|
||||
image_data = part.get("image_url", {})
|
||||
url = image_data.get("url", "") if isinstance(image_data, dict) else str(image_data)
|
||||
entry: Dict[str, Any] = {"type": "input_image", "image_url": url}
|
||||
# Preserve detail if specified
|
||||
detail = image_data.get("detail") if isinstance(image_data, dict) else None
|
||||
if detail:
|
||||
entry["detail"] = detail
|
||||
converted.append(entry)
|
||||
elif ptype in {"input_text", "input_image"}:
|
||||
# Already in Responses format — pass through
|
||||
converted.append(part)
|
||||
else:
|
||||
# Unknown content type — try to preserve as text
|
||||
text = part.get("text", "")
|
||||
if text:
|
||||
converted.append({"type": "input_text", "text": text})
|
||||
|
||||
return converted or ""
|
||||
|
||||
|
||||
class _CodexCompletionsAdapter:
|
||||
"""Drop-in shim that accepts chat.completions.create() kwargs and
|
||||
routes them through the Codex Responses streaming API."""
|
||||
@@ -697,26 +649,37 @@ class _CodexCompletionsAdapter:
|
||||
messages = kwargs.get("messages", [])
|
||||
model = kwargs.get("model", self._model)
|
||||
|
||||
# Separate system/instructions from conversation messages.
|
||||
# Convert chat.completions multimodal content blocks to Responses
|
||||
# API format (input_text / input_image instead of text / image_url).
|
||||
# Separate system/instructions from replayable conversation messages,
|
||||
# then route the rest through the SINGLE shared chat->Responses
|
||||
# converter used by the main agent transport
|
||||
# (agent/transports/codex.py). Maintaining a private conversion loop
|
||||
# here let chat-style messages with role="tool" leak straight into
|
||||
# Responses input[] — which the Responses API rejects with
|
||||
# "Invalid value: 'tool'. Supported values are: 'assistant', 'system',
|
||||
# 'developer', and 'user'." (issue #5709, hit hard by flush_memories()
|
||||
# / compression replaying real session history that includes assistant
|
||||
# tool_calls + role="tool" results). The shared converter encodes
|
||||
# assistant tool calls as `function_call` items and tool results as
|
||||
# `function_call_output` items with a valid call_id, so every
|
||||
# Responses path normalizes tool history identically and cannot drift.
|
||||
from agent.codex_responses_adapter import _chat_messages_to_responses_input
|
||||
|
||||
instructions = "You are a helpful assistant."
|
||||
input_msgs: List[Dict[str, Any]] = []
|
||||
replay_messages: List[Dict[str, Any]] = []
|
||||
for msg in messages:
|
||||
role = msg.get("role", "user")
|
||||
content = msg.get("content") or ""
|
||||
if role == "system":
|
||||
instructions = content if isinstance(content, str) else str(content)
|
||||
else:
|
||||
input_msgs.append({
|
||||
"role": role,
|
||||
"content": _convert_content_for_responses(content),
|
||||
})
|
||||
replay_messages.append(msg)
|
||||
|
||||
input_items = _chat_messages_to_responses_input(replay_messages)
|
||||
|
||||
resp_kwargs: Dict[str, Any] = {
|
||||
"model": model,
|
||||
"instructions": instructions,
|
||||
"input": input_msgs or [{"role": "user", "content": ""}],
|
||||
"input": input_items or [{"role": "user", "content": ""}],
|
||||
"store": False,
|
||||
}
|
||||
|
||||
|
||||
@@ -449,6 +449,17 @@ def _run_review_in_thread(
|
||||
# if a future code path bypasses the cache.
|
||||
review_agent.session_start = agent.session_start
|
||||
review_agent.session_id = agent.session_id
|
||||
# Never let the review fork compress. It shares the parent's
|
||||
# session_id, so if it won a compression race it would rotate the
|
||||
# parent into a NEW child that the gateway never adopts (the fork
|
||||
# is single-lifecycle and dies right after this run_conversation).
|
||||
# The foreground turn would then start from the stale parent and
|
||||
# compress it again, leaving the same parent with two sibling
|
||||
# children (issue #38727). Review also needs full context to
|
||||
# produce a good memory/skill summary — compressing would strip
|
||||
# detail. Both compression triggers in conversation_loop.py gate on
|
||||
# agent.compression_enabled, so this short-circuits both paths.
|
||||
review_agent.compression_enabled = False
|
||||
|
||||
from model_tools import get_tool_definitions
|
||||
from hermes_cli.plugins import (
|
||||
|
||||
@@ -553,6 +553,22 @@ class ContextCompressor(ContextEngine):
|
||||
self.last_rough_tokens_when_real_prompt_fit = 0
|
||||
self.awaiting_real_usage_after_compression = False
|
||||
|
||||
def on_session_end(self, session_id: str, messages: List[Dict[str, Any]]) -> None:
|
||||
"""Clear per-session compaction state at a real session boundary.
|
||||
|
||||
``_previous_summary`` is per-session iterative-summary state. It is
|
||||
cleared on ``on_session_reset()`` (/new, /reset), but session *end*
|
||||
(CLI exit, gateway expiry, session-id rotation) goes through
|
||||
``on_session_end()`` instead — which inherited a no-op from
|
||||
``ContextEngine``. Without clearing here, a cron/background session's
|
||||
summary could survive on a reused compressor instance and leak into the
|
||||
next live session via the ``_generate_summary()`` iterative-update path
|
||||
(#38788). ``compress()`` already guards the leak at the point of use;
|
||||
this is defense-in-depth that drops the stale summary the moment the
|
||||
owning session ends.
|
||||
"""
|
||||
self._previous_summary = None
|
||||
|
||||
def update_model(
|
||||
self,
|
||||
model: str,
|
||||
@@ -1818,6 +1834,41 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||
accumulated += msg_tokens
|
||||
cut_idx = i
|
||||
|
||||
# If the backward walk never broke early because the entire transcript
|
||||
# fits within soft_ceiling, accumulated now holds the total transcript
|
||||
# size. Without intervention _ensure_last_user_message_in_tail pushes
|
||||
# cut_idx forward to include the last user message, and the caller's
|
||||
# compress_start >= compress_end guard either returns unchanged (no-op)
|
||||
# or compresses a single message — both of which trigger the infinite
|
||||
# compaction loop described in #40803.
|
||||
#
|
||||
# Fix: when the whole transcript fits in soft_ceiling, compute a
|
||||
# meaningful cut point using the raw (non-inflated) budget so that
|
||||
# compression actually summarizes a worthwhile middle section.
|
||||
if cut_idx <= head_end and accumulated <= soft_ceiling and accumulated > 0:
|
||||
# The entire compressable region fits in the soft ceiling.
|
||||
# Re-walk with the raw budget (no 1.5x multiplier) to find a
|
||||
# split that gives the summarizer something useful.
|
||||
raw_budget = token_budget
|
||||
raw_accumulated = 0
|
||||
for j in range(n - 1, head_end - 1, -1):
|
||||
raw_msg = messages[j]
|
||||
raw_content = raw_msg.get("content") or ""
|
||||
raw_len = _content_length_for_budget(raw_content)
|
||||
raw_tok = raw_len // _CHARS_PER_TOKEN + 10
|
||||
for tc in raw_msg.get("tool_calls") or []:
|
||||
if isinstance(tc, dict):
|
||||
args = tc.get("function", {}).get("arguments", "")
|
||||
raw_tok += len(args) // _CHARS_PER_TOKEN
|
||||
if raw_accumulated + raw_tok > raw_budget and (n - j) >= min_tail:
|
||||
cut_idx = j
|
||||
break
|
||||
raw_accumulated += raw_tok
|
||||
cut_idx = j
|
||||
# If the raw-budget walk also consumed everything (very small
|
||||
# transcript), fall through — the existing fallback logic below
|
||||
# will still force a minimal cut after head_end.
|
||||
|
||||
# Ensure we protect at least min_tail messages
|
||||
fallback_cut = n - min_tail
|
||||
cut_idx = min(cut_idx, fallback_cut)
|
||||
@@ -1920,6 +1971,21 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||
compress_end = self._find_tail_cut_by_tokens(messages, compress_start)
|
||||
|
||||
if compress_start >= compress_end:
|
||||
# No compressable window — the entire transcript fits within
|
||||
# the tail budget (soft_ceiling). Without recording this as
|
||||
# an ineffective compression the anti-thrashing guard in
|
||||
# should_compress() never fires and every subsequent turn
|
||||
# re-triggers a no-op compression loop. (#40803)
|
||||
self._ineffective_compression_count += 1
|
||||
self._last_compression_savings_pct = 0.0
|
||||
if not self.quiet_mode:
|
||||
logger.warning(
|
||||
"Compression skipped: compress_start (%d) >= compress_end (%d) "
|
||||
"— transcript fits within tail budget, nothing to compress. "
|
||||
"ineffective_compression_count=%d",
|
||||
compress_start, compress_end,
|
||||
self._ineffective_compression_count,
|
||||
)
|
||||
return messages
|
||||
|
||||
turns_to_summarize = messages[compress_start:compress_end]
|
||||
@@ -1940,6 +2006,13 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||
if summary_body and not self._previous_summary:
|
||||
self._previous_summary = summary_body
|
||||
turns_to_summarize = messages[max(compress_start, summary_idx + 1):compress_end]
|
||||
elif self._previous_summary:
|
||||
# No handoff summary found in the current messages, but
|
||||
# _previous_summary is non-empty — it was set by a different
|
||||
# (now-ended) session (e.g., a cron job, a prior /new). Discard
|
||||
# it so _generate_summary() does not inject cross-session content
|
||||
# into the summarizer prompt via the iterative-update path.
|
||||
self._previous_summary = None
|
||||
|
||||
if not self.quiet_mode:
|
||||
logger.info(
|
||||
|
||||
@@ -507,12 +507,29 @@ def compress_context(
|
||||
agent._session_db.end_session(agent.session_id, "compression")
|
||||
old_session_id = agent.session_id
|
||||
agent.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
|
||||
# Ordering contract: the agent thread updates the contextvar here;
|
||||
# the gateway propagates to SessionEntry after run_in_executor returns.
|
||||
try:
|
||||
from gateway.session_context import set_current_session_id
|
||||
|
||||
set_current_session_id(agent.session_id)
|
||||
except Exception:
|
||||
os.environ["HERMES_SESSION_ID"] = agent.session_id
|
||||
# The gateway/tools session context (ContextVar + env) and the
|
||||
# logging session context are SEPARATE mechanisms. The call above
|
||||
# moves the former; the ``[session_id]`` tag on log lines comes
|
||||
# from ``hermes_logging._session_context`` (set once per turn in
|
||||
# conversation_loop.py). Without this, post-rotation log lines in
|
||||
# the same turn keep the STALE old id while the message/DB/gateway
|
||||
# state carry the new one — breaking log correlation exactly at the
|
||||
# compaction boundary (see #34089). Guarded separately so a logging
|
||||
# failure can never regress the routing update above.
|
||||
try:
|
||||
from hermes_logging import set_session_context
|
||||
|
||||
set_session_context(agent.session_id)
|
||||
except Exception:
|
||||
pass
|
||||
agent._session_db_created = False
|
||||
agent._session_db.create_session(
|
||||
session_id=agent.session_id,
|
||||
|
||||
@@ -31,6 +31,8 @@ from agent.codex_responses_adapter import _summarize_user_message_for_log
|
||||
from agent.display import KawaiiSpinner
|
||||
from agent.error_classifier import FailoverReason, classify_api_error
|
||||
from agent.iteration_budget import IterationBudget
|
||||
from agent.turn_context import build_turn_context
|
||||
from agent.turn_retry_state import TurnRetryState
|
||||
from agent.memory_manager import build_memory_context_block
|
||||
from agent.message_sanitization import (
|
||||
_repair_tool_call_arguments,
|
||||
@@ -55,7 +57,11 @@ from agent.process_bootstrap import _install_safe_stdio
|
||||
from agent.prompt_caching import apply_anthropic_cache_control
|
||||
from agent.retry_utils import jittered_backoff
|
||||
from agent.trajectory import has_incomplete_scratchpad
|
||||
from agent.usage_pricing import estimate_usage_cost, normalize_usage
|
||||
from agent.usage_pricing import (
|
||||
estimate_usage_cost,
|
||||
extract_provider_cost_usd,
|
||||
normalize_usage,
|
||||
)
|
||||
from hermes_constants import PARTIAL_STREAM_STUB_ID
|
||||
from hermes_logging import set_session_context
|
||||
from tools.skill_provenance import set_current_write_origin
|
||||
@@ -63,6 +69,11 @@ from utils import base_url_host_matches, env_var_enabled
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Stable prefix of the local interrupt status string emitted when a turn is
|
||||
# cancelled while waiting on the provider. Surfaces (ACP, TUI) match on this
|
||||
# to treat it as cancellation metadata rather than assistant prose.
|
||||
INTERRUPT_WAITING_FOR_MODEL_PREFIX = "Operation interrupted: waiting for model response ("
|
||||
|
||||
|
||||
def _ollama_context_limit_error(agent: Any, request_tokens: int) -> Optional[str]:
|
||||
"""Return a user-facing error when Ollama is loaded with too little context."""
|
||||
@@ -389,376 +400,43 @@ def run_conversation(
|
||||
Returns:
|
||||
Dict: Complete conversation result with final response and message history
|
||||
"""
|
||||
# Guard stdio against OSError from broken pipes (systemd/headless/daemon).
|
||||
# Installed once, transparent when streams are healthy, prevents crash on write.
|
||||
_install_safe_stdio()
|
||||
|
||||
agent._ensure_db_session()
|
||||
|
||||
# Tell auxiliary_client what the live main provider/model are for
|
||||
# this turn. Used by tools whose behaviour depends on the active
|
||||
# main model (e.g. vision_analyze's native fast path) so they see
|
||||
# the CLI/gateway override instead of the stale config.yaml
|
||||
# default. Idempotent — fine to call every turn.
|
||||
try:
|
||||
from agent.auxiliary_client import set_runtime_main
|
||||
set_runtime_main(
|
||||
getattr(agent, "provider", "") or "",
|
||||
getattr(agent, "model", "") or "",
|
||||
base_url=getattr(agent, "base_url", "") or "",
|
||||
api_key=getattr(agent, "api_key", "") or "",
|
||||
api_mode=getattr(agent, "api_mode", "") or "",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Tag all log records on this thread with the session ID so
|
||||
# ``hermes logs --session <id>`` can filter a single conversation.
|
||||
set_session_context(agent.session_id)
|
||||
|
||||
# Bind the skill write-origin ContextVar for this thread so tool
|
||||
# handlers (e.g. skill_manage create) can tell whether they are
|
||||
# running inside the background agent-improvement review fork vs.
|
||||
# a foreground user-directed turn. Set at the top of each call;
|
||||
# the review fork runs on its own thread with a fresh context,
|
||||
# so the foreground value here does not leak into it.
|
||||
set_current_write_origin(getattr(agent, "_memory_write_origin", "assistant_tool"))
|
||||
|
||||
# If the previous turn activated fallback, restore the primary
|
||||
# runtime so this turn gets a fresh attempt with the preferred model.
|
||||
# No-op when _fallback_activated is False (gateway, first turn, etc.).
|
||||
agent._restore_primary_runtime()
|
||||
|
||||
# Sanitize surrogate characters from user input. Clipboard paste from
|
||||
# rich-text editors (Google Docs, Word, etc.) can inject lone surrogates
|
||||
# that are invalid UTF-8 and crash JSON serialization in the OpenAI SDK.
|
||||
if isinstance(user_message, str):
|
||||
user_message = _sanitize_surrogates(user_message)
|
||||
if isinstance(persist_user_message, str):
|
||||
persist_user_message = _sanitize_surrogates(persist_user_message)
|
||||
|
||||
# Store stream callback for _interruptible_api_call to pick up
|
||||
agent._stream_callback = stream_callback
|
||||
agent._persist_user_message_idx = None
|
||||
agent._persist_user_message_override = persist_user_message
|
||||
# Generate unique task_id if not provided to isolate VMs between concurrent tasks
|
||||
effective_task_id = task_id or str(uuid.uuid4())
|
||||
# Expose the active task_id so tools running mid-turn (e.g. delegate_task
|
||||
# in delegate_tool.py) can identify this agent for the cross-agent file
|
||||
# state registry. Set BEFORE any tool dispatch so snapshots taken at
|
||||
# child-launch time see the parent's real id, not None.
|
||||
agent._current_task_id = effective_task_id
|
||||
turn_id = f"{agent.session_id or 'session'}:{effective_task_id}:{uuid.uuid4().hex[:8]}"
|
||||
agent._current_turn_id = turn_id
|
||||
agent._current_api_request_id = ""
|
||||
|
||||
# Reset retry counters and iteration budget at the start of each turn
|
||||
# so subagent usage from a previous turn doesn't eat into the next one.
|
||||
agent._invalid_tool_retries = 0
|
||||
agent._invalid_json_retries = 0
|
||||
agent._empty_content_retries = 0
|
||||
agent._incomplete_scratchpad_retries = 0
|
||||
agent._codex_incomplete_retries = 0
|
||||
agent._thinking_prefill_retries = 0
|
||||
agent._post_tool_empty_retried = False
|
||||
agent._last_content_with_tools = None
|
||||
agent._last_content_tools_all_housekeeping = False
|
||||
agent._mute_post_response = False
|
||||
agent._unicode_sanitization_passes = 0
|
||||
agent._tool_guardrails.reset_for_turn()
|
||||
agent._tool_guardrail_halt_decision = None
|
||||
# True until the server rejects an image_url content part with an error
|
||||
# like "Only 'text' content type is supported." Set to False on first
|
||||
# rejection and kept False for the rest of the session so we never re-send
|
||||
# images to a text-only endpoint. Scoped per `_run()` call, not per instance.
|
||||
agent._vision_supported = True
|
||||
|
||||
# Pre-turn connection health check: detect and clean up dead TCP
|
||||
# connections left over from provider outages or dropped streams.
|
||||
# This prevents the next API call from hanging on a zombie socket.
|
||||
if agent.api_mode != "anthropic_messages":
|
||||
try:
|
||||
if agent._cleanup_dead_connections():
|
||||
agent._emit_status(
|
||||
"🔌 Detected stale connections from a previous provider "
|
||||
"issue — cleaned up automatically. Proceeding with fresh "
|
||||
"connection."
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Replay compression warning through status_callback for gateway
|
||||
# platforms (the callback was not wired during __init__).
|
||||
if agent._compression_warning:
|
||||
agent._replay_compression_warning()
|
||||
agent._compression_warning = None # send once
|
||||
|
||||
# NOTE: _turns_since_memory and _iters_since_skill are NOT reset here.
|
||||
# They are initialized in __init__ and must persist across run_conversation
|
||||
# calls so that nudge logic accumulates correctly in CLI mode.
|
||||
agent.iteration_budget = IterationBudget(agent.max_iterations)
|
||||
|
||||
# Log conversation turn start for debugging/observability
|
||||
_preview_text = _summarize_user_message_for_log(user_message)
|
||||
_msg_preview = (_preview_text[:80] + "...") if len(_preview_text) > 80 else _preview_text
|
||||
_msg_preview = _msg_preview.replace("\n", " ")
|
||||
logger.info(
|
||||
"conversation turn: session=%s model=%s provider=%s platform=%s history=%d msg=%r",
|
||||
agent.session_id or "none", agent.model, agent.provider or "unknown",
|
||||
agent.platform or "unknown", len(conversation_history or []),
|
||||
_msg_preview,
|
||||
# ── Per-turn setup (the prologue) ──
|
||||
# All once-per-turn setup — stdio guarding, retry-counter resets, user
|
||||
# message sanitization, todo/nudge hydration, system-prompt restore-or-
|
||||
# build, crash-resilience persistence, preflight compression, the
|
||||
# ``pre_llm_call`` plugin hook, and external-memory prefetch — lives in
|
||||
# ``build_turn_context``. It mutates ``agent`` exactly as the inline code
|
||||
# did and returns the locals the loop below reads back. See
|
||||
# ``agent/turn_context.py``.
|
||||
_ctx = build_turn_context(
|
||||
agent,
|
||||
user_message,
|
||||
system_message,
|
||||
conversation_history,
|
||||
task_id,
|
||||
stream_callback,
|
||||
persist_user_message,
|
||||
restore_or_build_system_prompt=_restore_or_build_system_prompt,
|
||||
install_safe_stdio=_install_safe_stdio,
|
||||
sanitize_surrogates=_sanitize_surrogates,
|
||||
summarize_user_message_for_log=_summarize_user_message_for_log,
|
||||
set_session_context=set_session_context,
|
||||
set_current_write_origin=set_current_write_origin,
|
||||
ra=_ra,
|
||||
)
|
||||
user_message = _ctx.user_message
|
||||
original_user_message = _ctx.original_user_message
|
||||
messages = _ctx.messages
|
||||
conversation_history = _ctx.conversation_history
|
||||
active_system_prompt = _ctx.active_system_prompt
|
||||
effective_task_id = _ctx.effective_task_id
|
||||
turn_id = _ctx.turn_id
|
||||
current_turn_user_idx = _ctx.current_turn_user_idx
|
||||
_should_review_memory = _ctx.should_review_memory
|
||||
_plugin_user_context = _ctx.plugin_user_context
|
||||
_ext_prefetch_cache = _ctx.ext_prefetch_cache
|
||||
|
||||
# Initialize conversation (copy to avoid mutating the caller's list)
|
||||
messages = list(conversation_history) if conversation_history else []
|
||||
|
||||
# Hydrate todo store from conversation history (gateway creates a fresh
|
||||
# AIAgent per message, so the in-memory store is empty -- we need to
|
||||
# recover the todo state from the most recent todo tool response in history)
|
||||
if conversation_history and not agent._todo_store.has_items():
|
||||
agent._hydrate_todo_store(conversation_history)
|
||||
|
||||
# Hydrate per-session nudge counters from persisted history.
|
||||
# Gateway creates a fresh AIAgent per inbound message (cache miss /
|
||||
# 1h idle eviction / config-signature mismatch / process restart), so
|
||||
# _turns_since_memory and _user_turn_count start at 0 every turn and
|
||||
# the memory.nudge_interval trigger may never be reached. Reconstruct
|
||||
# an effective count from prior user turns in conversation_history.
|
||||
# Idempotent: a cached agent that already accumulated counters keeps
|
||||
# them; only a freshly-built agent with empty in-memory state hydrates.
|
||||
# See issue #22357.
|
||||
if conversation_history and agent._user_turn_count == 0:
|
||||
prior_user_turns = sum(
|
||||
1 for m in conversation_history if m.get("role") == "user"
|
||||
)
|
||||
if prior_user_turns > 0:
|
||||
agent._user_turn_count = prior_user_turns
|
||||
if agent._memory_nudge_interval > 0 and agent._turns_since_memory == 0:
|
||||
# % preserves original 1-in-N cadence rather than firing a
|
||||
# review immediately on resume (which would surprise users
|
||||
# whose session happened to land just past a multiple of N).
|
||||
agent._turns_since_memory = prior_user_turns % agent._memory_nudge_interval
|
||||
|
||||
|
||||
# Prefill messages (few-shot priming) are injected at API-call time only,
|
||||
# never stored in the messages list. This keeps them ephemeral: they won't
|
||||
# be saved to session DB, session logs, or batch trajectories, but they're
|
||||
# automatically re-applied on every API call (including session continuations).
|
||||
|
||||
# Track user turns for memory flush and periodic nudge logic
|
||||
agent._user_turn_count += 1
|
||||
|
||||
# Reset the streaming context scrubber at the top of each turn so a
|
||||
# hung span from a prior interrupted stream can't taint this turn's
|
||||
# output.
|
||||
scrubber = getattr(agent, "_stream_context_scrubber", None)
|
||||
if scrubber is not None:
|
||||
scrubber.reset()
|
||||
# Reset the think scrubber for the same reason — an interrupted
|
||||
# prior stream may have left us inside an unterminated block.
|
||||
think_scrubber = getattr(agent, "_stream_think_scrubber", None)
|
||||
if think_scrubber is not None:
|
||||
think_scrubber.reset()
|
||||
|
||||
# Preserve the original user message (no nudge injection).
|
||||
original_user_message = persist_user_message if persist_user_message is not None else user_message
|
||||
|
||||
# Track memory nudge trigger (turn-based, checked here).
|
||||
# Skill trigger is checked AFTER the agent loop completes, based on
|
||||
# how many tool iterations THIS turn used.
|
||||
_should_review_memory = False
|
||||
if (agent._memory_nudge_interval > 0
|
||||
and "memory" in agent.valid_tool_names
|
||||
and agent._memory_store):
|
||||
agent._turns_since_memory += 1
|
||||
if agent._turns_since_memory >= agent._memory_nudge_interval:
|
||||
_should_review_memory = True
|
||||
agent._turns_since_memory = 0
|
||||
|
||||
# Add user message
|
||||
user_msg = {"role": "user", "content": user_message}
|
||||
messages.append(user_msg)
|
||||
current_turn_user_idx = len(messages) - 1
|
||||
agent._persist_user_message_idx = current_turn_user_idx
|
||||
|
||||
if not agent.quiet_mode:
|
||||
_print_preview = _summarize_user_message_for_log(user_message)
|
||||
agent._safe_print(f"💬 Starting conversation: '{_print_preview[:60]}{'...' if len(_print_preview) > 60 else ''}'")
|
||||
|
||||
# ── System prompt (cached per session for prefix caching) ──
|
||||
# Built once on first call, reused for all subsequent calls.
|
||||
# Only rebuilt after context compression events (which invalidate
|
||||
# the cache and reload memory from disk).
|
||||
#
|
||||
# For continuing sessions (gateway creates a fresh AIAgent per
|
||||
# message), we load the stored system prompt from the session DB
|
||||
# instead of rebuilding. Rebuilding would pick up memory changes
|
||||
# from disk that the model already knows about (it wrote them!),
|
||||
# producing a different system prompt and breaking the Anthropic
|
||||
# prefix cache.
|
||||
if agent._cached_system_prompt is None:
|
||||
_restore_or_build_system_prompt(agent, system_message, conversation_history)
|
||||
|
||||
active_system_prompt = agent._cached_system_prompt
|
||||
|
||||
# Crash-resilience: persist the inbound user turn as soon as the session row
|
||||
# has a valid system prompt, before any provider call or tool execution can
|
||||
# hang/kill the process. The normal end-of-turn persist still runs later;
|
||||
# _last_flushed_db_idx makes this idempotent and prevents duplicate rows.
|
||||
try:
|
||||
agent._persist_session(messages, conversation_history)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Early turn-start session persistence failed for session=%s",
|
||||
agent.session_id or "none",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# ── Preflight context compression ──
|
||||
# Before entering the main loop, check if the loaded conversation
|
||||
# history already exceeds the model's context threshold. This handles
|
||||
# cases where a user switches to a model with a smaller context window
|
||||
# while having a large existing session — compress proactively rather
|
||||
# than waiting for an API error (which might be caught as a non-retryable
|
||||
# 4xx and abort the request entirely).
|
||||
if (
|
||||
agent.compression_enabled
|
||||
and len(messages) > agent.context_compressor.protect_first_n
|
||||
+ agent.context_compressor.protect_last_n + 1
|
||||
):
|
||||
# Include tool schema tokens — with many tools these can add
|
||||
# 20-30K+ tokens that the old sys+msg estimate missed entirely.
|
||||
_preflight_tokens = estimate_request_tokens_rough(
|
||||
messages,
|
||||
system_prompt=active_system_prompt or "",
|
||||
tools=agent.tools or None,
|
||||
)
|
||||
_compressor = agent.context_compressor
|
||||
_defer_preflight = getattr(
|
||||
_compressor,
|
||||
"should_defer_preflight_to_real_usage",
|
||||
lambda _tokens: False,
|
||||
)
|
||||
_preflight_deferred = _defer_preflight(_preflight_tokens)
|
||||
|
||||
if not _preflight_deferred:
|
||||
# Keep the CLI/ACP context display in sync with what preflight
|
||||
# actually measured. The status bar reads
|
||||
# ``compressor.last_prompt_tokens``, which otherwise only updates
|
||||
# from a *successful* API response. When the conversation has grown
|
||||
# since the last successful call — or when compression then fails
|
||||
# (e.g. the auxiliary summary model times out) and no fresh usage
|
||||
# arrives — the bar stays stuck at the old, smaller value while
|
||||
# preflight reports a much larger number, looking out of sync.
|
||||
# Seed it with the fresh estimate (only ever revising upward; a real
|
||||
# ``update_from_response`` will correct it after the next API call).
|
||||
# Skipped when deferring — a deferred estimate is known to over-count
|
||||
# vs the last real provider prompt, so trusting it for the display
|
||||
# would re-introduce the very desync we're avoiding.
|
||||
_last = _compressor.last_prompt_tokens
|
||||
# Do NOT overwrite the -1 sentinel. compress_context() sets
|
||||
# last_prompt_tokens=-1 right after compression to mark "no real API
|
||||
# usage yet". `(x or 0)` evaluates to -1 (truthy) for the sentinel,
|
||||
# so the old comparison was always True and clobbered the sentinel
|
||||
# with a schema-inflated rough estimate — re-triggering compression
|
||||
# on the next turn (#36718). Treat any negative value as "no data".
|
||||
if _last >= 0 and _preflight_tokens > _last:
|
||||
_compressor.last_prompt_tokens = _preflight_tokens
|
||||
|
||||
if _preflight_deferred:
|
||||
logger.info(
|
||||
"Skipping preflight compression: rough estimate ~%s >= %s, "
|
||||
"but last real provider prompt was %s after compression",
|
||||
f"{_preflight_tokens:,}",
|
||||
f"{_compressor.threshold_tokens:,}",
|
||||
f"{_compressor.last_real_prompt_tokens:,}",
|
||||
)
|
||||
elif _compressor.should_compress(_preflight_tokens):
|
||||
logger.info(
|
||||
"Preflight compression: ~%s tokens >= %s threshold (model %s, ctx %s)",
|
||||
f"{_preflight_tokens:,}",
|
||||
f"{_compressor.threshold_tokens:,}",
|
||||
agent.model,
|
||||
f"{_compressor.context_length:,}",
|
||||
)
|
||||
agent._emit_status(
|
||||
f"📦 Preflight compression: ~{_preflight_tokens:,} tokens "
|
||||
f">= {_compressor.threshold_tokens:,} threshold. "
|
||||
"This may take a moment."
|
||||
)
|
||||
# May need multiple passes for very large sessions with small
|
||||
# context windows (each pass summarises the middle N turns).
|
||||
for _pass in range(3):
|
||||
_orig_len = len(messages)
|
||||
messages, active_system_prompt = agent._compress_context(
|
||||
messages, system_message, approx_tokens=_preflight_tokens,
|
||||
task_id=effective_task_id,
|
||||
)
|
||||
if len(messages) >= _orig_len:
|
||||
break # Cannot compress further
|
||||
# Compression created a new session — clear the history
|
||||
# reference so _flush_messages_to_session_db writes ALL
|
||||
# compressed messages to the new session's SQLite, not
|
||||
# skipping them because conversation_history is still the
|
||||
# pre-compression length.
|
||||
conversation_history = None
|
||||
# Fix: reset retry counters after compression so the model
|
||||
# gets a fresh budget on the compressed context. Without
|
||||
# this, pre-compression retries carry over and the model
|
||||
# hits "(empty)" immediately after compression-induced
|
||||
# context loss.
|
||||
agent._empty_content_retries = 0
|
||||
agent._thinking_prefill_retries = 0
|
||||
agent._last_content_with_tools = None
|
||||
agent._last_content_tools_all_housekeeping = False
|
||||
agent._mute_post_response = False
|
||||
# Re-estimate after compression
|
||||
_preflight_tokens = estimate_request_tokens_rough(
|
||||
messages,
|
||||
system_prompt=active_system_prompt or "",
|
||||
tools=agent.tools or None,
|
||||
)
|
||||
if not _compressor.should_compress(_preflight_tokens):
|
||||
break # Under threshold or anti-thrash guard stopped it
|
||||
|
||||
# Plugin hook: pre_llm_call
|
||||
# Fired once per turn before the tool-calling loop. Plugins can
|
||||
# return a dict with a ``context`` key (or a plain string) whose
|
||||
# value is appended to the current turn's user message.
|
||||
#
|
||||
# Context is ALWAYS injected into the user message, never the
|
||||
# system prompt. This preserves the prompt cache prefix — the
|
||||
# system prompt stays identical across turns so cached tokens
|
||||
# are reused. The system prompt is Hermes's territory; plugins
|
||||
# contribute context alongside the user's input.
|
||||
#
|
||||
# All injected context is ephemeral (not persisted to session DB).
|
||||
_plugin_user_context = ""
|
||||
try:
|
||||
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
||||
_pre_results = _invoke_hook(
|
||||
"pre_llm_call",
|
||||
session_id=agent.session_id,
|
||||
task_id=effective_task_id,
|
||||
turn_id=turn_id,
|
||||
user_message=original_user_message,
|
||||
conversation_history=list(messages),
|
||||
is_first_turn=(not bool(conversation_history)),
|
||||
model=agent.model,
|
||||
platform=getattr(agent, "platform", None) or "",
|
||||
sender_id=getattr(agent, "_user_id", None) or "",
|
||||
)
|
||||
_ctx_parts: list[str] = []
|
||||
for r in _pre_results:
|
||||
if isinstance(r, dict) and r.get("context"):
|
||||
_ctx_parts.append(str(r["context"]))
|
||||
elif isinstance(r, str) and r.strip():
|
||||
_ctx_parts.append(r)
|
||||
if _ctx_parts:
|
||||
_plugin_user_context = "\n\n".join(_ctx_parts)
|
||||
except Exception as exc:
|
||||
logger.warning("pre_llm_call hook failed: %s", exc)
|
||||
|
||||
# Main conversation loop
|
||||
# Main conversation loop counters (pure locals consumed by the loop below).
|
||||
api_call_count = 0
|
||||
final_response = None
|
||||
interrupted = False
|
||||
@@ -770,53 +448,6 @@ def run_conversation(
|
||||
compression_attempts = 0
|
||||
_turn_exit_reason = "unknown" # Diagnostic: why the loop ended
|
||||
|
||||
# Per-turn file-mutation verifier state. Keyed by resolved path;
|
||||
# each failed ``write_file`` / ``patch`` call records the error
|
||||
# preview. Later successful writes to the same path remove the
|
||||
# entry (the model recovered). At end-of-turn, any entries still
|
||||
# present are surfaced in an advisory footer so the model cannot
|
||||
# over-claim success while the file is actually unchanged on disk.
|
||||
agent._turn_failed_file_mutations: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# Record the execution thread so interrupt()/clear_interrupt() can
|
||||
# scope the tool-level interrupt signal to THIS agent's thread only.
|
||||
# Must be set before any thread-scoped interrupt syncing.
|
||||
agent._execution_thread_id = threading.current_thread().ident
|
||||
|
||||
# Always clear stale per-thread state from a previous turn. If an
|
||||
# interrupt arrived before startup finished, preserve it and bind it
|
||||
# to this execution thread now instead of dropping it on the floor.
|
||||
_ra()._set_interrupt(False, agent._execution_thread_id)
|
||||
if agent._interrupt_requested:
|
||||
_ra()._set_interrupt(True, agent._execution_thread_id)
|
||||
agent._interrupt_thread_signal_pending = False
|
||||
else:
|
||||
agent._interrupt_message = None
|
||||
agent._interrupt_thread_signal_pending = False
|
||||
|
||||
# Notify memory providers of the new turn so cadence tracking works.
|
||||
# Must happen BEFORE prefetch_all() so providers know which turn it is
|
||||
# and can gate context/dialectic refresh via contextCadence/dialecticCadence.
|
||||
if agent._memory_manager:
|
||||
try:
|
||||
_turn_msg = original_user_message if isinstance(original_user_message, str) else ""
|
||||
agent._memory_manager.on_turn_start(agent._user_turn_count, _turn_msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# External memory provider: prefetch once before the tool loop.
|
||||
# Reuse the cached result on every iteration to avoid re-calling
|
||||
# prefetch_all() on each tool call (10 tool calls = 10x latency + cost).
|
||||
# Use original_user_message (clean input) — user_message may contain
|
||||
# injected skill content that bloats / breaks provider queries.
|
||||
_ext_prefetch_cache = ""
|
||||
if agent._memory_manager:
|
||||
try:
|
||||
_query = original_user_message if isinstance(original_user_message, str) else ""
|
||||
_ext_prefetch_cache = agent._memory_manager.prefetch_all(_query) or ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Optional opt-in runtime: if api_mode == codex_app_server, hand the
|
||||
# turn to the codex app-server subprocess (terminal/file ops/patching
|
||||
# all run inside Codex). Default Hermes path is bypassed entirely.
|
||||
@@ -1172,22 +803,8 @@ def run_conversation(
|
||||
api_start_time = time.time()
|
||||
retry_count = 0
|
||||
max_retries = agent._api_max_retries
|
||||
primary_recovery_attempted = False
|
||||
_retry = TurnRetryState()
|
||||
max_compression_attempts = 3
|
||||
codex_auth_retry_attempted=False
|
||||
anthropic_auth_retry_attempted=False
|
||||
nous_auth_retry_attempted=False
|
||||
nous_paid_entitlement_refresh_attempted=False
|
||||
copilot_auth_retry_attempted=False
|
||||
thinking_sig_retry_attempted = False
|
||||
invalid_encrypted_content_retry_attempted = False
|
||||
image_shrink_retry_attempted = False
|
||||
multimodal_tool_content_retry_attempted = False
|
||||
oauth_1m_beta_retry_attempted = False
|
||||
llama_cpp_grammar_retry_attempted = False
|
||||
has_retried_429 = False
|
||||
restart_with_compressed_messages = False
|
||||
restart_with_length_continuation = False
|
||||
|
||||
finish_reason = "stop"
|
||||
response = None # Guard against UnboundLocalError if all retries fail
|
||||
@@ -1220,7 +837,7 @@ def run_conversation(
|
||||
if agent._try_activate_fallback():
|
||||
retry_count = 0
|
||||
compression_attempts = 0
|
||||
primary_recovery_attempted = False
|
||||
_retry.primary_recovery_attempted = False
|
||||
continue
|
||||
# No fallback available — surface buffered context
|
||||
# so user sees the rate-limit message that led here.
|
||||
@@ -1545,7 +1162,7 @@ def run_conversation(
|
||||
if agent._try_activate_fallback():
|
||||
retry_count = 0
|
||||
compression_attempts = 0
|
||||
primary_recovery_attempted = False
|
||||
_retry.primary_recovery_attempted = False
|
||||
continue
|
||||
|
||||
# Check for error field in response (some providers include this)
|
||||
@@ -1616,7 +1233,7 @@ def run_conversation(
|
||||
if agent._try_activate_fallback():
|
||||
retry_count = 0
|
||||
compression_attempts = 0
|
||||
primary_recovery_attempted = False
|
||||
_retry.primary_recovery_attempted = False
|
||||
continue
|
||||
# Terminal — flush buffered retry trace so user sees what happened.
|
||||
agent._flush_status_buffer()
|
||||
@@ -1840,7 +1457,7 @@ def run_conversation(
|
||||
}
|
||||
messages.append(continue_msg)
|
||||
agent._session_messages = messages
|
||||
restart_with_length_continuation = True
|
||||
_retry.restart_with_length_continuation = True
|
||||
break
|
||||
|
||||
partial_response = agent._strip_think_blocks("".join(truncated_response_parts)).strip()
|
||||
@@ -2020,6 +1637,37 @@ def run_conversation(
|
||||
agent.session_cost_status = cost_result.status
|
||||
agent.session_cost_source = cost_result.source
|
||||
|
||||
# ── Real provider-REPORTED cost (never estimated) ──
|
||||
# OpenRouter usage accounting returns ``usage.cost`` on the
|
||||
# response when the request carries usage:{include:true}
|
||||
# (added on OpenRouter routes). When the provider reports
|
||||
# nothing, this stays None — absent, NOT zero — so cost
|
||||
# displays hide instead of showing a fabricated $0.00.
|
||||
reported_cost_usd = extract_provider_cost_usd(response.usage)
|
||||
if reported_cost_usd is not None:
|
||||
_prev_actual = getattr(agent, "session_actual_cost_usd", None)
|
||||
agent.session_actual_cost_usd = (_prev_actual or 0.0) + reported_cost_usd
|
||||
agent.session_cost_status = "actual"
|
||||
agent.session_cost_source = "provider_cost_api"
|
||||
|
||||
# Per-model session breakdown for /usage — counts are always
|
||||
# real; cost_usd only accumulates provider-reported values
|
||||
# and stays None when the provider reports nothing.
|
||||
_model_usage = getattr(agent, "session_model_usage", None)
|
||||
if _model_usage is None:
|
||||
_model_usage = agent.session_model_usage = {}
|
||||
_mrow = _model_usage.setdefault(agent.model, {
|
||||
"calls": 0, "input": 0, "output": 0,
|
||||
"cache_read": 0, "cache_write": 0, "cost_usd": None,
|
||||
})
|
||||
_mrow["calls"] += 1
|
||||
_mrow["input"] += canonical_usage.input_tokens
|
||||
_mrow["output"] += canonical_usage.output_tokens
|
||||
_mrow["cache_read"] += canonical_usage.cache_read_tokens
|
||||
_mrow["cache_write"] += canonical_usage.cache_write_tokens
|
||||
if reported_cost_usd is not None:
|
||||
_mrow["cost_usd"] = (_mrow["cost_usd"] or 0.0) + reported_cost_usd
|
||||
|
||||
# Persist token counts to session DB for /insights.
|
||||
# Do this for every platform with a session_id so non-CLI
|
||||
# sessions (gateway, cron, delegated runs) cannot lose
|
||||
@@ -2046,8 +1694,14 @@ def run_conversation(
|
||||
reasoning_tokens=canonical_usage.reasoning_tokens,
|
||||
estimated_cost_usd=float(cost_result.amount_usd)
|
||||
if cost_result.amount_usd is not None else None,
|
||||
cost_status=cost_result.status,
|
||||
cost_source=cost_result.source,
|
||||
# Provider-reported per-call cost delta. NULL
|
||||
# (not 0) when the provider reported nothing —
|
||||
# the SQL CASE keeps actual_cost_usd untouched.
|
||||
actual_cost_usd=reported_cost_usd,
|
||||
cost_status="actual"
|
||||
if reported_cost_usd is not None else cost_result.status,
|
||||
cost_source="provider_cost_api"
|
||||
if reported_cost_usd is not None else cost_result.source,
|
||||
billing_provider=agent.provider,
|
||||
billing_base_url=agent.base_url,
|
||||
billing_mode="subscription_included"
|
||||
@@ -2089,7 +1743,7 @@ def run_conversation(
|
||||
f"({hit_pct:.0f}% hit, {written:,} written)"
|
||||
)
|
||||
|
||||
has_retried_429 = False # Reset on success
|
||||
_retry.has_retried_429 = False # Reset on success
|
||||
# Note: don't clear the retry buffer here — an "API call
|
||||
# success" only means we got bytes back, not that we got
|
||||
# usable content. Empty responses still loop through the
|
||||
@@ -2117,7 +1771,7 @@ def run_conversation(
|
||||
agent._vprint(f"{agent.log_prefix}⚡ Interrupted during API call.", force=True)
|
||||
agent._persist_session(messages, conversation_history)
|
||||
interrupted = True
|
||||
final_response = f"Operation interrupted: waiting for model response ({api_elapsed:.1f}s elapsed)."
|
||||
final_response = f"{INTERRUPT_WAITING_FOR_MODEL_PREFIX}{api_elapsed:.1f}s elapsed)."
|
||||
break
|
||||
|
||||
except Exception as api_error:
|
||||
@@ -2419,9 +2073,9 @@ def run_conversation(
|
||||
getattr(agent, "provider", "") or "",
|
||||
getattr(agent, "base_url", "") or "",
|
||||
)
|
||||
and not nous_paid_entitlement_refresh_attempted
|
||||
and not _retry.nous_paid_entitlement_refresh_attempted
|
||||
):
|
||||
nous_paid_entitlement_refresh_attempted = True
|
||||
_retry.nous_paid_entitlement_refresh_attempted = True
|
||||
if _try_refresh_nous_paid_entitlement_credentials(agent):
|
||||
agent._vprint(
|
||||
f"{agent.log_prefix}🔐 Nous paid access verified — "
|
||||
@@ -2430,9 +2084,9 @@ def run_conversation(
|
||||
)
|
||||
continue
|
||||
|
||||
recovered_with_pool, has_retried_429 = agent._recover_with_credential_pool(
|
||||
recovered_with_pool, _retry.has_retried_429 = agent._recover_with_credential_pool(
|
||||
status_code=status_code,
|
||||
has_retried_429=has_retried_429,
|
||||
has_retried_429=_retry.has_retried_429,
|
||||
classified_reason=classified.reason,
|
||||
error_context=error_context,
|
||||
)
|
||||
@@ -2447,9 +2101,9 @@ def run_conversation(
|
||||
# fails, fall through to normal error handling.
|
||||
if (
|
||||
classified.reason == FailoverReason.image_too_large
|
||||
and not image_shrink_retry_attempted
|
||||
and not _retry.image_shrink_retry_attempted
|
||||
):
|
||||
image_shrink_retry_attempted = True
|
||||
_retry.image_shrink_retry_attempted = True
|
||||
if agent._try_shrink_image_parts_in_messages(api_messages):
|
||||
agent._vprint(
|
||||
f"{agent.log_prefix}📐 Image(s) exceeded provider size limit — "
|
||||
@@ -2472,9 +2126,9 @@ def run_conversation(
|
||||
# downgrade, and retry once. See issue #27344.
|
||||
if (
|
||||
classified.reason == FailoverReason.multimodal_tool_content_unsupported
|
||||
and not multimodal_tool_content_retry_attempted
|
||||
and not _retry.multimodal_tool_content_retry_attempted
|
||||
):
|
||||
multimodal_tool_content_retry_attempted = True
|
||||
_retry.multimodal_tool_content_retry_attempted = True
|
||||
if agent._try_strip_image_parts_from_tool_messages(api_messages):
|
||||
agent._vprint(
|
||||
f"{agent.log_prefix}📐 Provider rejected list-type tool content — "
|
||||
@@ -2501,9 +2155,9 @@ def run_conversation(
|
||||
classified.reason == FailoverReason.oauth_long_context_beta_forbidden
|
||||
and agent.api_mode == "anthropic_messages"
|
||||
and agent._is_anthropic_oauth
|
||||
and not oauth_1m_beta_retry_attempted
|
||||
and not _retry.oauth_1m_beta_retry_attempted
|
||||
):
|
||||
oauth_1m_beta_retry_attempted = True
|
||||
_retry.oauth_1m_beta_retry_attempted = True
|
||||
if not getattr(agent, "_oauth_1m_beta_disabled", False):
|
||||
agent._oauth_1m_beta_disabled = True
|
||||
try:
|
||||
@@ -2522,9 +2176,9 @@ def run_conversation(
|
||||
agent.api_mode == "codex_responses"
|
||||
and agent.provider in {"openai-codex", "xai-oauth"}
|
||||
and status_code == 401
|
||||
and not codex_auth_retry_attempted
|
||||
and not _retry.codex_auth_retry_attempted
|
||||
):
|
||||
codex_auth_retry_attempted = True
|
||||
_retry.codex_auth_retry_attempted = True
|
||||
if agent._try_refresh_codex_client_credentials(force=True):
|
||||
_label = "xAI OAuth" if agent.provider == "xai-oauth" else "Codex"
|
||||
agent._buffer_vprint(f"🔐 {_label} auth refreshed after 401. Retrying request...")
|
||||
@@ -2533,9 +2187,9 @@ def run_conversation(
|
||||
agent.api_mode == "chat_completions"
|
||||
and agent.provider == "nous"
|
||||
and status_code == 401
|
||||
and not nous_auth_retry_attempted
|
||||
and not _retry.nous_auth_retry_attempted
|
||||
):
|
||||
nous_auth_retry_attempted = True
|
||||
_retry.nous_auth_retry_attempted = True
|
||||
if agent._try_refresh_nous_client_credentials(force=True):
|
||||
print(f"{agent.log_prefix}🔐 Nous agent key refreshed after 401. Retrying request...")
|
||||
continue
|
||||
@@ -2564,9 +2218,9 @@ def run_conversation(
|
||||
if (
|
||||
agent.provider == "copilot"
|
||||
and status_code == 401
|
||||
and not copilot_auth_retry_attempted
|
||||
and not _retry.copilot_auth_retry_attempted
|
||||
):
|
||||
copilot_auth_retry_attempted = True
|
||||
_retry.copilot_auth_retry_attempted = True
|
||||
if agent._try_refresh_copilot_client_credentials():
|
||||
agent._buffer_vprint(f"🔐 Copilot credentials refreshed after 401. Retrying request...")
|
||||
continue
|
||||
@@ -2574,9 +2228,9 @@ def run_conversation(
|
||||
agent.api_mode == "anthropic_messages"
|
||||
and status_code == 401
|
||||
and hasattr(agent, '_anthropic_api_key')
|
||||
and not anthropic_auth_retry_attempted
|
||||
and not _retry.anthropic_auth_retry_attempted
|
||||
):
|
||||
anthropic_auth_retry_attempted = True
|
||||
_retry.anthropic_auth_retry_attempted = True
|
||||
from agent.anthropic_adapter import _is_oauth_token
|
||||
from agent.azure_identity_adapter import is_token_provider
|
||||
if agent._try_refresh_anthropic_client_credentials():
|
||||
@@ -2617,9 +2271,9 @@ def run_conversation(
|
||||
# blocks at all. One-shot — don't retry infinitely.
|
||||
if (
|
||||
classified.reason == FailoverReason.thinking_signature
|
||||
and not thinking_sig_retry_attempted
|
||||
and not _retry.thinking_sig_retry_attempted
|
||||
):
|
||||
thinking_sig_retry_attempted = True
|
||||
_retry.thinking_sig_retry_attempted = True
|
||||
for _m in messages:
|
||||
if isinstance(_m, dict):
|
||||
_m.pop("reasoning_details", None)
|
||||
@@ -2651,7 +2305,7 @@ def run_conversation(
|
||||
# handles it (the provider is rejecting something else).
|
||||
if (
|
||||
classified.reason == FailoverReason.invalid_encrypted_content
|
||||
and not invalid_encrypted_content_retry_attempted
|
||||
and not _retry.invalid_encrypted_content_retry_attempted
|
||||
and agent.api_mode == "codex_responses"
|
||||
and bool(getattr(agent, "_codex_reasoning_replay_enabled", True))
|
||||
and any(
|
||||
@@ -2662,7 +2316,7 @@ def run_conversation(
|
||||
for _m in messages
|
||||
)
|
||||
):
|
||||
invalid_encrypted_content_retry_attempted = True
|
||||
_retry.invalid_encrypted_content_retry_attempted = True
|
||||
replay_stats = agent._disable_codex_reasoning_replay(messages)
|
||||
agent._vprint(
|
||||
f"{agent.log_prefix}⚠️ Encrypted reasoning replay was rejected by the provider — "
|
||||
@@ -2689,9 +2343,9 @@ def run_conversation(
|
||||
# fires only for users on llama.cpp's OAI server.
|
||||
if (
|
||||
classified.reason == FailoverReason.llama_cpp_grammar_pattern
|
||||
and not llama_cpp_grammar_retry_attempted
|
||||
and not _retry.llama_cpp_grammar_retry_attempted
|
||||
):
|
||||
llama_cpp_grammar_retry_attempted = True
|
||||
_retry.llama_cpp_grammar_retry_attempted = True
|
||||
try:
|
||||
from tools.schema_sanitizer import strip_pattern_and_format
|
||||
_, _stripped = strip_pattern_and_format(agent.tools)
|
||||
@@ -2902,7 +2556,7 @@ def run_conversation(
|
||||
f"(was {old_ctx:,}), retrying..."
|
||||
)
|
||||
time.sleep(2)
|
||||
restart_with_compressed_messages = True
|
||||
_retry.restart_with_compressed_messages = True
|
||||
break
|
||||
# Fall through to normal error handling if compression
|
||||
# is exhausted or didn't help.
|
||||
@@ -2935,7 +2589,7 @@ def run_conversation(
|
||||
if agent._try_activate_fallback(reason=classified.reason):
|
||||
retry_count = 0
|
||||
compression_attempts = 0
|
||||
primary_recovery_attempted = False
|
||||
_retry.primary_recovery_attempted = False
|
||||
continue
|
||||
|
||||
# ── Nous Portal: record rate limit & skip retries ─────
|
||||
@@ -3073,7 +2727,7 @@ def run_conversation(
|
||||
if len(messages) < original_len:
|
||||
agent._buffer_status(f"🗜️ Compressed {original_len} → {len(messages)} messages, retrying...")
|
||||
time.sleep(2) # Brief pause between compression retries
|
||||
restart_with_compressed_messages = True
|
||||
_retry.restart_with_compressed_messages = True
|
||||
break
|
||||
else:
|
||||
# Terminal — surface buffered context so the user
|
||||
@@ -3145,7 +2799,7 @@ def run_conversation(
|
||||
"failed": True,
|
||||
"compression_exhausted": True,
|
||||
}
|
||||
restart_with_compressed_messages = True
|
||||
_retry.restart_with_compressed_messages = True
|
||||
break
|
||||
|
||||
# Error is about the INPUT being too large. Only reduce
|
||||
@@ -3230,7 +2884,7 @@ def run_conversation(
|
||||
if len(messages) < original_len:
|
||||
agent._buffer_status(f"🗜️ Compressed {original_len} → {len(messages)} messages, retrying...")
|
||||
time.sleep(2) # Brief pause between compression retries
|
||||
restart_with_compressed_messages = True
|
||||
_retry.restart_with_compressed_messages = True
|
||||
break
|
||||
else:
|
||||
# Can't compress further and already at minimum tier
|
||||
@@ -3335,7 +2989,7 @@ def run_conversation(
|
||||
if agent._try_activate_fallback():
|
||||
retry_count = 0
|
||||
compression_attempts = 0
|
||||
primary_recovery_attempted = False
|
||||
_retry.primary_recovery_attempted = False
|
||||
continue
|
||||
if api_kwargs is not None:
|
||||
agent._dump_api_request_debug(
|
||||
@@ -3467,10 +3121,10 @@ def run_conversation(
|
||||
# client once for transient transport errors (stale
|
||||
# connection pool, TCP reset). Only attempted once
|
||||
# per API call block.
|
||||
if not primary_recovery_attempted and agent._try_recover_primary_transport(
|
||||
if not _retry.primary_recovery_attempted and agent._try_recover_primary_transport(
|
||||
api_error, retry_count=retry_count, max_retries=max_retries,
|
||||
):
|
||||
primary_recovery_attempted = True
|
||||
_retry.primary_recovery_attempted = True
|
||||
retry_count = 0
|
||||
continue
|
||||
# Try fallback before giving up entirely
|
||||
@@ -3479,7 +3133,7 @@ def run_conversation(
|
||||
if agent._try_activate_fallback():
|
||||
retry_count = 0
|
||||
compression_attempts = 0
|
||||
primary_recovery_attempted = False
|
||||
_retry.primary_recovery_attempted = False
|
||||
continue
|
||||
# Terminal — flush buffered retry/fallback trace.
|
||||
agent._flush_status_buffer()
|
||||
@@ -3630,17 +3284,17 @@ def run_conversation(
|
||||
_turn_exit_reason = "interrupted_during_api_call"
|
||||
break
|
||||
|
||||
if restart_with_compressed_messages:
|
||||
if _retry.restart_with_compressed_messages:
|
||||
api_call_count -= 1
|
||||
agent.iteration_budget.refund()
|
||||
# Count compression restarts toward the retry limit to prevent
|
||||
# infinite loops when compression reduces messages but not enough
|
||||
# to fit the context window.
|
||||
retry_count += 1
|
||||
restart_with_compressed_messages = False
|
||||
_retry.restart_with_compressed_messages = False
|
||||
continue
|
||||
|
||||
if restart_with_length_continuation:
|
||||
if _retry.restart_with_length_continuation:
|
||||
# Progressively boost the output token budget on each retry.
|
||||
# Retry 1 → 2× base, retry 2 → 3× base, capped at 32 768.
|
||||
# Applies to all providers via _ephemeral_max_output_tokens.
|
||||
|
||||
@@ -374,7 +374,7 @@ def _iter_custom_providers(config: Optional[dict] = None):
|
||||
yield _normalize_custom_pool_name(name), entry
|
||||
|
||||
|
||||
def get_custom_provider_pool_key(base_url: str, provider_name: Optional[str] = None) -> Optional[str]:
|
||||
def get_custom_provider_pool_key(base_url: Optional[str], provider_name: Optional[str] = None) -> Optional[str]:
|
||||
"""Look up the custom_providers list in config.yaml and return 'custom:<name>' for a matching base_url.
|
||||
|
||||
When provider_name is given, prefer matching by name first (solving the case where
|
||||
|
||||
@@ -375,6 +375,11 @@ CURATOR_REVIEW_PROMPT = (
|
||||
"into ~/.hermes/skills/.archive/) is the maximum destructive action. "
|
||||
"Archives are recoverable; deletion is not.\n"
|
||||
"3. DO NOT touch skills shown as pinned=yes. Skip them entirely.\n"
|
||||
"3b. DO NOT archive, delete, consolidate, move, or otherwise modify any "
|
||||
"skill named in the protected built-ins list (currently: plan). These "
|
||||
"back load-bearing UX (slash-command entry points referenced in docs and "
|
||||
"tips) and are filtered out of the candidate list below — never resurrect "
|
||||
"one as an archive or absorb target.\n"
|
||||
"4. DO NOT use usage counters as a reason to skip consolidation. The "
|
||||
"counters are new and often mostly zero. Judge overlap on CONTENT, "
|
||||
"not on use_count. 'use=0' is not evidence a skill is valuable; it's "
|
||||
|
||||
@@ -219,6 +219,35 @@ def _supports_vision_override(
|
||||
coerced = _coerce_capability_bool(per_model.get("supports_vision"))
|
||||
if coerced is not None:
|
||||
return coerced
|
||||
|
||||
# 2b. Legacy list-style custom_providers. Entries are dicts with a
|
||||
# "name" key and a nested "models" dict. Match by provider name (which
|
||||
# may appear as the raw name or "custom:<name>" at runtime).
|
||||
custom_providers = cfg.get("custom_providers")
|
||||
if isinstance(custom_providers, list):
|
||||
# Build candidate names: the provider value and the config provider
|
||||
# value, both raw and with "custom:" prefix stripped/added.
|
||||
candidate_names: set = set()
|
||||
for p in filter(None, (provider, config_provider)):
|
||||
candidate_names.add(p)
|
||||
if p.startswith("custom:"):
|
||||
candidate_names.add(p[len("custom:"):])
|
||||
else:
|
||||
candidate_names.add(f"custom:{p}")
|
||||
for entry_raw in custom_providers:
|
||||
if not isinstance(entry_raw, dict):
|
||||
continue
|
||||
entry_name = str(entry_raw.get("name") or "").strip()
|
||||
if entry_name not in candidate_names:
|
||||
continue
|
||||
models_raw = entry_raw.get("models")
|
||||
models_cfg = models_raw if isinstance(models_raw, dict) else {}
|
||||
per_model_raw = models_cfg.get(model)
|
||||
per_model = per_model_raw if isinstance(per_model_raw, dict) else {}
|
||||
coerced = _coerce_capability_bool(per_model.get("supports_vision"))
|
||||
if coerced is not None:
|
||||
return coerced
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -20,23 +20,17 @@ import json
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from agent.usage_pricing import (
|
||||
CanonicalUsage,
|
||||
DEFAULT_PRICING,
|
||||
estimate_usage_cost,
|
||||
format_duration_compact,
|
||||
has_known_pricing,
|
||||
)
|
||||
|
||||
_DEFAULT_PRICING = DEFAULT_PRICING
|
||||
|
||||
|
||||
def _has_known_pricing(model_name: str, provider: str = None, base_url: str = None) -> bool:
|
||||
"""Check if a model has known pricing (vs unknown/custom endpoint)."""
|
||||
return has_known_pricing(model_name, provider=provider, base_url=base_url)
|
||||
|
||||
|
||||
def _estimate_cost(
|
||||
session_or_model: Dict[str, Any] | str,
|
||||
@@ -45,8 +39,8 @@ def _estimate_cost(
|
||||
*,
|
||||
cache_read_tokens: int = 0,
|
||||
cache_write_tokens: int = 0,
|
||||
provider: str = None,
|
||||
base_url: str = None,
|
||||
provider: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
) -> tuple[float, str]:
|
||||
"""Estimate the USD cost for a session row or a model/token tuple."""
|
||||
if isinstance(session_or_model, dict):
|
||||
@@ -77,9 +71,6 @@ def _estimate_cost(
|
||||
return float(result.amount_usd or 0.0), result.status
|
||||
|
||||
|
||||
def _format_duration(seconds: float) -> str:
|
||||
"""Format seconds into a human-readable duration string."""
|
||||
return format_duration_compact(seconds)
|
||||
|
||||
|
||||
def _bar_chart(values: List[int], max_width: int = 20) -> List[str]:
|
||||
@@ -435,7 +426,7 @@ class InsightsEngine:
|
||||
included_cost_sessions += 1
|
||||
elif status == "unknown":
|
||||
unknown_cost_sessions += 1
|
||||
if _has_known_pricing(model, s.get("billing_provider"), s.get("billing_base_url")):
|
||||
if has_known_pricing(model, s.get("billing_provider"), s.get("billing_base_url")):
|
||||
models_with_pricing.add(display)
|
||||
else:
|
||||
models_without_pricing.add(display)
|
||||
@@ -508,7 +499,7 @@ class InsightsEngine:
|
||||
d["tool_calls"] += s.get("tool_call_count") or 0
|
||||
estimate, status = _estimate_cost(s)
|
||||
d["cost"] += estimate
|
||||
d["has_pricing"] = _has_known_pricing(model, s.get("billing_provider"), s.get("billing_base_url"))
|
||||
d["has_pricing"] = has_known_pricing(model, s.get("billing_provider"), s.get("billing_base_url"))
|
||||
d["cost_status"] = status
|
||||
|
||||
result = [
|
||||
@@ -679,7 +670,7 @@ class InsightsEngine:
|
||||
top.append({
|
||||
"label": "Longest session",
|
||||
"session_id": longest["id"][:16],
|
||||
"value": _format_duration(dur),
|
||||
"value": format_duration_compact(dur),
|
||||
"date": datetime.fromtimestamp(longest["started_at"]).strftime("%b %d"),
|
||||
})
|
||||
|
||||
@@ -764,7 +755,7 @@ class InsightsEngine:
|
||||
lines.append(f" Input tokens: {o['total_input_tokens']:<12,} Output tokens: {o['total_output_tokens']:,}")
|
||||
lines.append(f" Total tokens: {o['total_tokens']:,}")
|
||||
if o["total_hours"] > 0:
|
||||
lines.append(f" Active time: ~{_format_duration(o['total_hours'] * 3600):<11} Avg session: ~{_format_duration(o['avg_session_duration'])}")
|
||||
lines.append(f" Active time: ~{format_duration_compact(o['total_hours'] * 3600):<11} Avg session: ~{format_duration_compact(o['avg_session_duration'])}")
|
||||
lines.append(f" Avg msgs/session: {o['avg_messages_per_session']:.1f}")
|
||||
lines.append("")
|
||||
|
||||
@@ -879,7 +870,7 @@ class InsightsEngine:
|
||||
lines.append(f"**Sessions:** {o['total_sessions']} | **Messages:** {o['total_messages']:,} | **Tool calls:** {o['total_tool_calls']:,}")
|
||||
lines.append(f"**Tokens:** {o['total_tokens']:,} (in: {o['total_input_tokens']:,} / out: {o['total_output_tokens']:,})")
|
||||
if o["total_hours"] > 0:
|
||||
lines.append(f"**Active time:** ~{_format_duration(o['total_hours'] * 3600)} | **Avg session:** ~{_format_duration(o['avg_session_duration'])}")
|
||||
lines.append(f"**Active time:** ~{format_duration_compact(o['total_hours'] * 3600)} | **Avg session:** ~{format_duration_compact(o['avg_session_duration'])}")
|
||||
lines.append("")
|
||||
|
||||
# Models (top 5)
|
||||
|
||||
@@ -1684,6 +1684,26 @@ def get_model_context_length(
|
||||
"in config.yaml to override.",
|
||||
model, base_url, f"{DEFAULT_FALLBACK_CONTEXT:,}",
|
||||
)
|
||||
# 3b. Before falling back to the hard 256K default, consult the
|
||||
# hardcoded catalog as a last resort. A proxied/custom Anthropic
|
||||
# gateway (e.g. corporate proxy) fails the Ollama/local probes
|
||||
# above, but the model name may still match an entry in
|
||||
# DEFAULT_CONTEXT_LENGTHS (e.g. "claude-opus-4-8" → 1M).
|
||||
# Without this, the early return here short-circuits the catalog
|
||||
# lookup at step 8 and silently caps context at 256K.
|
||||
model_lower = model.lower()
|
||||
for default_model, length in sorted(
|
||||
DEFAULT_CONTEXT_LENGTHS.items(),
|
||||
key=lambda x: len(x[0]),
|
||||
reverse=True,
|
||||
):
|
||||
if default_model in model_lower:
|
||||
logger.info(
|
||||
"Using hardcoded context length %s for model %r "
|
||||
"(custom endpoint, catalog match on %r)",
|
||||
f"{length:,}", model, default_model,
|
||||
)
|
||||
return length
|
||||
return DEFAULT_FALLBACK_CONTEXT
|
||||
|
||||
# 4. Anthropic /v1/models API (only for regular API keys, not OAuth)
|
||||
|
||||
@@ -388,6 +388,13 @@ class ChatCompletionsTransport(ProviderTransport):
|
||||
if provider_prefs and is_openrouter:
|
||||
extra_body["provider"] = provider_prefs
|
||||
|
||||
# OpenRouter usage accounting — response `usage.cost` carries the REAL
|
||||
# charged cost (credits are 1:1 USD). Parity with the profile path in
|
||||
# plugins/model-providers/openrouter/__init__.py; this branch only runs
|
||||
# when the OpenRouter profile isn't loaded.
|
||||
if is_openrouter:
|
||||
extra_body["usage"] = {"include": True}
|
||||
|
||||
# Pareto Code router plugin — model-gated. Same shape as the
|
||||
# profile path in plugins/model-providers/openrouter/__init__.py;
|
||||
# this branch only runs when the OpenRouter profile isn't loaded.
|
||||
|
||||
388
agent/turn_context.py
Normal file
@@ -0,0 +1,388 @@
|
||||
"""Per-turn setup for ``run_conversation`` (the turn prologue).
|
||||
|
||||
``run_conversation`` opened with ~470 lines of straight-line setup before the
|
||||
tool-calling loop ever started: stdio guarding, runtime-main wiring, retry-counter
|
||||
resets, user-message sanitization, todo/nudge-counter hydration, system-prompt
|
||||
restore-or-build, crash-resilience persistence, preflight context compression, the
|
||||
``pre_llm_call`` plugin hook, and external-memory prefetch.
|
||||
|
||||
All of that is *prologue* — it runs once per turn, has no back-references into the
|
||||
loop, and produces a fixed set of values the loop then consumes. ``TurnContext``
|
||||
captures those produced values; ``build_turn_context`` performs the setup work and
|
||||
returns one. ``run_conversation`` is left to unpack the context and run the loop,
|
||||
shrinking the orchestrator by the full prologue.
|
||||
|
||||
The builder still mutates ``agent`` heavily (counters, thread id, cached prompt,
|
||||
session DB) exactly as the inline code did — those side effects are the point. The
|
||||
``TurnContext`` it returns carries only the *locals* the loop reads back.
|
||||
|
||||
Behavior is identical to the original inline prologue; this is a pure
|
||||
move-and-name refactor with no semantic change.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from agent.iteration_budget import IterationBudget
|
||||
from agent.model_metadata import estimate_request_tokens_rough
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnContext:
|
||||
"""Values produced by the turn prologue and consumed by the turn loop."""
|
||||
|
||||
# Sanitized inbound message (surrogates stripped).
|
||||
user_message: str
|
||||
# Clean message preserved for transcripts / memory queries (no nudge injection).
|
||||
original_user_message: Any
|
||||
# Working message list for this turn (loop appends to it).
|
||||
messages: List[Dict[str, Any]]
|
||||
# May be reset to None by preflight compression (new session created).
|
||||
conversation_history: Optional[List[Dict[str, Any]]]
|
||||
# Cached system prompt active for this turn (may be rebuilt by compression).
|
||||
active_system_prompt: Optional[str]
|
||||
# Task / turn identifiers.
|
||||
effective_task_id: str
|
||||
turn_id: str
|
||||
# Index of the current user turn within ``messages``.
|
||||
current_turn_user_idx: int
|
||||
# Whether the post-turn memory review should fire.
|
||||
should_review_memory: bool = False
|
||||
# Context contributed by ``pre_llm_call`` plugins (appended to user message).
|
||||
plugin_user_context: str = ""
|
||||
# External-memory prefetch result, reused across loop iterations.
|
||||
ext_prefetch_cache: str = ""
|
||||
|
||||
|
||||
def build_turn_context(
|
||||
agent,
|
||||
user_message: str,
|
||||
system_message: Optional[str],
|
||||
conversation_history: Optional[List[Dict[str, Any]]],
|
||||
task_id: Optional[str],
|
||||
stream_callback,
|
||||
persist_user_message: Optional[str],
|
||||
*,
|
||||
restore_or_build_system_prompt,
|
||||
install_safe_stdio,
|
||||
sanitize_surrogates,
|
||||
summarize_user_message_for_log,
|
||||
set_session_context,
|
||||
set_current_write_origin,
|
||||
ra,
|
||||
) -> TurnContext:
|
||||
"""Run the once-per-turn setup and return the loop's input context.
|
||||
|
||||
The callables/helpers the original prologue referenced from the
|
||||
``conversation_loop`` module are passed in explicitly to keep this module
|
||||
free of an import cycle with ``agent.conversation_loop``.
|
||||
"""
|
||||
# Guard stdio against OSError from broken pipes (systemd/headless/daemon).
|
||||
install_safe_stdio()
|
||||
|
||||
agent._ensure_db_session()
|
||||
|
||||
# Tell auxiliary_client what the live main provider/model are for this turn.
|
||||
try:
|
||||
from agent.auxiliary_client import set_runtime_main
|
||||
set_runtime_main(
|
||||
getattr(agent, "provider", "") or "",
|
||||
getattr(agent, "model", "") or "",
|
||||
base_url=getattr(agent, "base_url", "") or "",
|
||||
api_key=getattr(agent, "api_key", "") or "",
|
||||
api_mode=getattr(agent, "api_mode", "") or "",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Tag log records on this thread with the session ID for ``hermes logs``.
|
||||
set_session_context(agent.session_id)
|
||||
|
||||
# Bind the skill write-origin ContextVar for this thread.
|
||||
set_current_write_origin(getattr(agent, "_memory_write_origin", "assistant_tool"))
|
||||
|
||||
# Restore the primary runtime if the previous turn activated fallback.
|
||||
agent._restore_primary_runtime()
|
||||
|
||||
# Sanitize surrogate characters from user input.
|
||||
if isinstance(user_message, str):
|
||||
user_message = sanitize_surrogates(user_message)
|
||||
if isinstance(persist_user_message, str):
|
||||
persist_user_message = sanitize_surrogates(persist_user_message)
|
||||
|
||||
# Store stream callback for _interruptible_api_call to pick up.
|
||||
agent._stream_callback = stream_callback
|
||||
agent._persist_user_message_idx = None
|
||||
agent._persist_user_message_override = persist_user_message
|
||||
# Generate unique task_id if not provided to isolate VMs between tasks.
|
||||
effective_task_id = task_id or str(uuid.uuid4())
|
||||
agent._current_task_id = effective_task_id
|
||||
turn_id = f"{agent.session_id or 'session'}:{effective_task_id}:{uuid.uuid4().hex[:8]}"
|
||||
agent._current_turn_id = turn_id
|
||||
agent._current_api_request_id = ""
|
||||
|
||||
# Reset retry counters and iteration budget at the start of each turn.
|
||||
agent._invalid_tool_retries = 0
|
||||
agent._invalid_json_retries = 0
|
||||
agent._empty_content_retries = 0
|
||||
agent._incomplete_scratchpad_retries = 0
|
||||
agent._codex_incomplete_retries = 0
|
||||
agent._thinking_prefill_retries = 0
|
||||
agent._post_tool_empty_retried = False
|
||||
agent._last_content_with_tools = None
|
||||
agent._last_content_tools_all_housekeeping = False
|
||||
agent._mute_post_response = False
|
||||
agent._unicode_sanitization_passes = 0
|
||||
agent._tool_guardrails.reset_for_turn()
|
||||
agent._tool_guardrail_halt_decision = None
|
||||
agent._vision_supported = True
|
||||
|
||||
# Pre-turn connection health check: clean up dead TCP connections.
|
||||
if agent.api_mode != "anthropic_messages":
|
||||
try:
|
||||
if agent._cleanup_dead_connections():
|
||||
agent._emit_status(
|
||||
"🔌 Detected stale connections from a previous provider "
|
||||
"issue — cleaned up automatically. Proceeding with fresh "
|
||||
"connection."
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Replay compression warning through status_callback for gateway platforms.
|
||||
if agent._compression_warning:
|
||||
agent._replay_compression_warning()
|
||||
agent._compression_warning = None # send once
|
||||
|
||||
# NOTE: _turns_since_memory and _iters_since_skill are NOT reset here.
|
||||
agent.iteration_budget = IterationBudget(agent.max_iterations)
|
||||
|
||||
# Log conversation turn start for debugging/observability.
|
||||
_preview_text = summarize_user_message_for_log(user_message)
|
||||
_msg_preview = (_preview_text[:80] + "...") if len(_preview_text) > 80 else _preview_text
|
||||
_msg_preview = _msg_preview.replace("\n", " ")
|
||||
logger.info(
|
||||
"conversation turn: session=%s model=%s provider=%s platform=%s history=%d msg=%r",
|
||||
agent.session_id or "none", agent.model, agent.provider or "unknown",
|
||||
agent.platform or "unknown", len(conversation_history or []),
|
||||
_msg_preview,
|
||||
)
|
||||
|
||||
# Initialize conversation (copy to avoid mutating the caller's list).
|
||||
messages = list(conversation_history) if conversation_history else []
|
||||
|
||||
# Hydrate todo store from conversation history.
|
||||
if conversation_history and not agent._todo_store.has_items():
|
||||
agent._hydrate_todo_store(conversation_history)
|
||||
|
||||
# Hydrate per-session nudge counters from persisted history (issue #22357).
|
||||
if conversation_history and agent._user_turn_count == 0:
|
||||
prior_user_turns = sum(
|
||||
1 for m in conversation_history if m.get("role") == "user"
|
||||
)
|
||||
if prior_user_turns > 0:
|
||||
agent._user_turn_count = prior_user_turns
|
||||
if agent._memory_nudge_interval > 0 and agent._turns_since_memory == 0:
|
||||
agent._turns_since_memory = prior_user_turns % agent._memory_nudge_interval
|
||||
|
||||
# Track user turns for memory flush and periodic nudge logic.
|
||||
agent._user_turn_count += 1
|
||||
|
||||
# Reset the streaming context scrubber at the top of each turn.
|
||||
scrubber = getattr(agent, "_stream_context_scrubber", None)
|
||||
if scrubber is not None:
|
||||
scrubber.reset()
|
||||
# Reset the think scrubber for the same reason.
|
||||
think_scrubber = getattr(agent, "_stream_think_scrubber", None)
|
||||
if think_scrubber is not None:
|
||||
think_scrubber.reset()
|
||||
|
||||
# Preserve the original user message (no nudge injection).
|
||||
original_user_message = persist_user_message if persist_user_message is not None else user_message
|
||||
|
||||
# Track memory nudge trigger (turn-based, checked here).
|
||||
should_review_memory = False
|
||||
if (agent._memory_nudge_interval > 0
|
||||
and "memory" in agent.valid_tool_names
|
||||
and agent._memory_store):
|
||||
agent._turns_since_memory += 1
|
||||
if agent._turns_since_memory >= agent._memory_nudge_interval:
|
||||
should_review_memory = True
|
||||
agent._turns_since_memory = 0
|
||||
|
||||
# Add user message.
|
||||
user_msg = {"role": "user", "content": user_message}
|
||||
messages.append(user_msg)
|
||||
current_turn_user_idx = len(messages) - 1
|
||||
agent._persist_user_message_idx = current_turn_user_idx
|
||||
|
||||
if not agent.quiet_mode:
|
||||
_print_preview = summarize_user_message_for_log(user_message)
|
||||
agent._safe_print(
|
||||
f"💬 Starting conversation: '{_print_preview[:60]}"
|
||||
f"{'...' if len(_print_preview) > 60 else ''}'"
|
||||
)
|
||||
|
||||
# ── System prompt (cached per session for prefix caching) ──
|
||||
if agent._cached_system_prompt is None:
|
||||
restore_or_build_system_prompt(agent, system_message, conversation_history)
|
||||
|
||||
active_system_prompt = agent._cached_system_prompt
|
||||
|
||||
# Crash-resilience: persist the inbound user turn as soon as the session row exists.
|
||||
try:
|
||||
agent._persist_session(messages, conversation_history)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Early turn-start session persistence failed for session=%s",
|
||||
agent.session_id or "none",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# ── Preflight context compression ──
|
||||
if (
|
||||
agent.compression_enabled
|
||||
and len(messages) > agent.context_compressor.protect_first_n
|
||||
+ agent.context_compressor.protect_last_n + 1
|
||||
):
|
||||
_preflight_tokens = estimate_request_tokens_rough(
|
||||
messages,
|
||||
system_prompt=active_system_prompt or "",
|
||||
tools=agent.tools or None,
|
||||
)
|
||||
_compressor = agent.context_compressor
|
||||
_defer_preflight = getattr(
|
||||
_compressor,
|
||||
"should_defer_preflight_to_real_usage",
|
||||
lambda _tokens: False,
|
||||
)
|
||||
_preflight_deferred = _defer_preflight(_preflight_tokens)
|
||||
|
||||
if not _preflight_deferred:
|
||||
_last = _compressor.last_prompt_tokens
|
||||
# Do NOT overwrite the -1 sentinel (#36718).
|
||||
if _last >= 0 and _preflight_tokens > _last:
|
||||
_compressor.last_prompt_tokens = _preflight_tokens
|
||||
|
||||
if _preflight_deferred:
|
||||
logger.info(
|
||||
"Skipping preflight compression: rough estimate ~%s >= %s, "
|
||||
"but last real provider prompt was %s after compression",
|
||||
f"{_preflight_tokens:,}",
|
||||
f"{_compressor.threshold_tokens:,}",
|
||||
f"{_compressor.last_real_prompt_tokens:,}",
|
||||
)
|
||||
elif _compressor.should_compress(_preflight_tokens):
|
||||
logger.info(
|
||||
"Preflight compression: ~%s tokens >= %s threshold (model %s, ctx %s)",
|
||||
f"{_preflight_tokens:,}",
|
||||
f"{_compressor.threshold_tokens:,}",
|
||||
agent.model,
|
||||
f"{_compressor.context_length:,}",
|
||||
)
|
||||
agent._emit_status(
|
||||
f"📦 Preflight compression: ~{_preflight_tokens:,} tokens "
|
||||
f">= {_compressor.threshold_tokens:,} threshold. "
|
||||
"This may take a moment."
|
||||
)
|
||||
for _pass in range(3):
|
||||
_orig_len = len(messages)
|
||||
messages, active_system_prompt = agent._compress_context(
|
||||
messages, system_message, approx_tokens=_preflight_tokens,
|
||||
task_id=effective_task_id,
|
||||
)
|
||||
if len(messages) >= _orig_len:
|
||||
break # Cannot compress further
|
||||
conversation_history = None
|
||||
agent._empty_content_retries = 0
|
||||
agent._thinking_prefill_retries = 0
|
||||
agent._last_content_with_tools = None
|
||||
agent._last_content_tools_all_housekeeping = False
|
||||
agent._mute_post_response = False
|
||||
_preflight_tokens = estimate_request_tokens_rough(
|
||||
messages,
|
||||
system_prompt=active_system_prompt or "",
|
||||
tools=agent.tools or None,
|
||||
)
|
||||
if not _compressor.should_compress(_preflight_tokens):
|
||||
break
|
||||
|
||||
# Plugin hook: pre_llm_call (context injected into user message, not system prompt).
|
||||
plugin_user_context = ""
|
||||
try:
|
||||
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
||||
_pre_results = _invoke_hook(
|
||||
"pre_llm_call",
|
||||
session_id=agent.session_id,
|
||||
task_id=effective_task_id,
|
||||
turn_id=turn_id,
|
||||
user_message=original_user_message,
|
||||
conversation_history=list(messages),
|
||||
is_first_turn=(not bool(conversation_history)),
|
||||
model=agent.model,
|
||||
platform=getattr(agent, "platform", None) or "",
|
||||
sender_id=getattr(agent, "_user_id", None) or "",
|
||||
)
|
||||
_ctx_parts: list[str] = []
|
||||
for r in _pre_results:
|
||||
if isinstance(r, dict) and r.get("context"):
|
||||
_ctx_parts.append(str(r["context"]))
|
||||
elif isinstance(r, str) and r.strip():
|
||||
_ctx_parts.append(r)
|
||||
if _ctx_parts:
|
||||
plugin_user_context = "\n\n".join(_ctx_parts)
|
||||
except Exception as exc:
|
||||
logger.warning("pre_llm_call hook failed: %s", exc)
|
||||
|
||||
# Per-turn file-mutation verifier state.
|
||||
agent._turn_failed_file_mutations = {}
|
||||
|
||||
# Record the execution thread so interrupt()/clear_interrupt() can scope
|
||||
# the tool-level interrupt signal to THIS agent's thread only.
|
||||
agent._execution_thread_id = threading.current_thread().ident
|
||||
|
||||
# Clear stale per-thread interrupt state, preserving a pending interrupt.
|
||||
ra()._set_interrupt(False, agent._execution_thread_id)
|
||||
if agent._interrupt_requested:
|
||||
ra()._set_interrupt(True, agent._execution_thread_id)
|
||||
agent._interrupt_thread_signal_pending = False
|
||||
else:
|
||||
agent._interrupt_message = None
|
||||
agent._interrupt_thread_signal_pending = False
|
||||
|
||||
# Notify memory providers of the new turn (BEFORE prefetch_all).
|
||||
if agent._memory_manager:
|
||||
try:
|
||||
_turn_msg = original_user_message if isinstance(original_user_message, str) else ""
|
||||
agent._memory_manager.on_turn_start(agent._user_turn_count, _turn_msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# External memory provider: prefetch once before the tool loop.
|
||||
ext_prefetch_cache = ""
|
||||
if agent._memory_manager:
|
||||
try:
|
||||
_query = original_user_message if isinstance(original_user_message, str) else ""
|
||||
ext_prefetch_cache = agent._memory_manager.prefetch_all(_query) or ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return TurnContext(
|
||||
user_message=user_message,
|
||||
original_user_message=original_user_message,
|
||||
messages=messages,
|
||||
conversation_history=conversation_history,
|
||||
active_system_prompt=active_system_prompt,
|
||||
effective_task_id=effective_task_id,
|
||||
turn_id=turn_id,
|
||||
current_turn_user_idx=current_turn_user_idx,
|
||||
should_review_memory=should_review_memory,
|
||||
plugin_user_context=plugin_user_context,
|
||||
ext_prefetch_cache=ext_prefetch_cache,
|
||||
)
|
||||
68
agent/turn_retry_state.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Per-attempt recovery bookkeeping for the conversation turn loop.
|
||||
|
||||
The inner retry loop in ``run_conversation`` (``while retry_count <
|
||||
max_retries``) makes several distinct recovery attempts on a single model API
|
||||
call: a credential-pool 429 retry, a per-provider OAuth refresh (codex,
|
||||
anthropic, nous, copilot), a long-context compression restart, a length-
|
||||
continuation restart, and a handful of format-recovery branches (thinking-
|
||||
signature stripping, multimodal-tool-content stripping, llama.cpp grammar
|
||||
fallback, image shrink, invalid-encrypted-content, 1M-beta header).
|
||||
|
||||
Each of those branches is guarded by a one-shot boolean so it fires at most
|
||||
once per attempt. They used to be ~16 bare ``*_attempted`` / ``has_retried_*``
|
||||
/ ``restart_with_*`` locals declared inline before the loop and threaded
|
||||
through its 2,400-line body. ``TurnRetryState`` collapses them into one object
|
||||
the loop mutates in place (``state.codex_auth_retry_attempted = True``), giving
|
||||
the recovery bookkeeping a single named, testable home.
|
||||
|
||||
Loop-control variables (``retry_count``, ``max_retries``,
|
||||
``max_compression_attempts``) intentionally stay as plain locals — they are the
|
||||
``while`` mechanics, not recovery bookkeeping, and putting them on the object
|
||||
would add indirection without clarifying anything.
|
||||
|
||||
This module is dependency-free so it can be unit-tested in isolation and
|
||||
imported by the turn loop without an import cycle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, fields
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnRetryState:
|
||||
"""One-shot recovery guards + restart signals for a single API-call attempt.
|
||||
|
||||
A fresh instance is created for each iteration of the outer turn loop
|
||||
(once per ``api_call_count``). Each guard fires its recovery branch at most
|
||||
once; the ``restart_with_*`` signals are read by the loop after the attempt
|
||||
to decide whether to rebuild the request and retry.
|
||||
"""
|
||||
|
||||
# ── Per-provider OAuth / credential refresh guards ───────────────────
|
||||
codex_auth_retry_attempted: bool = False
|
||||
anthropic_auth_retry_attempted: bool = False
|
||||
nous_auth_retry_attempted: bool = False
|
||||
nous_paid_entitlement_refresh_attempted: bool = False
|
||||
copilot_auth_retry_attempted: bool = False
|
||||
|
||||
# ── Format / payload recovery guards ─────────────────────────────────
|
||||
thinking_sig_retry_attempted: bool = False
|
||||
invalid_encrypted_content_retry_attempted: bool = False
|
||||
image_shrink_retry_attempted: bool = False
|
||||
multimodal_tool_content_retry_attempted: bool = False
|
||||
oauth_1m_beta_retry_attempted: bool = False
|
||||
llama_cpp_grammar_retry_attempted: bool = False
|
||||
|
||||
# ── Transport / rate-limit recovery ──────────────────────────────────
|
||||
primary_recovery_attempted: bool = False
|
||||
has_retried_429: bool = False
|
||||
|
||||
# ── Restart signals (read by the outer loop after the attempt) ───────
|
||||
restart_with_compressed_messages: bool = False
|
||||
restart_with_length_continuation: bool = False
|
||||
|
||||
def __iter__(self):
|
||||
# Convenience for debugging / tests: iterate (name, value) pairs.
|
||||
for f in fields(self):
|
||||
yield f.name, getattr(self, f.name)
|
||||
@@ -849,6 +849,73 @@ def estimate_usage_cost(
|
||||
)
|
||||
|
||||
|
||||
def _finite_nonneg_number(value: Any) -> Optional[float]:
|
||||
"""Return ``value`` as a float when it is a real, finite, non-negative
|
||||
number (int/float, not bool); otherwise None."""
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
return None
|
||||
try:
|
||||
f = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if f != f or f in (float("inf"), float("-inf")) or f < 0:
|
||||
return None
|
||||
return f
|
||||
|
||||
|
||||
def extract_provider_cost_usd(response_usage: Any) -> Optional[float]:
|
||||
"""Provider-REPORTED cost (USD) from a response ``usage`` object, or None.
|
||||
|
||||
Reads the ``usage.cost`` field that OpenRouter's usage accounting returns
|
||||
(``usage: {"include": true}`` request param; OpenRouter credits are 1:1
|
||||
USD). OpenRouter-compatible aggregators use the same field. This NEVER
|
||||
estimates: when the provider reports nothing, the result is None — callers
|
||||
must treat None as "no cost data", not zero. A reported ``0`` is a real
|
||||
zero (e.g. free-tier models) and is returned as ``0.0``.
|
||||
"""
|
||||
if response_usage is None:
|
||||
return None
|
||||
cost = getattr(response_usage, "cost", None)
|
||||
if cost is None and isinstance(response_usage, dict):
|
||||
cost = response_usage.get("cost")
|
||||
return _finite_nonneg_number(cost)
|
||||
|
||||
|
||||
def real_session_cost_usd(agent: Any) -> Optional[float]:
|
||||
"""Session-cumulative provider-REPORTED cost in USD, or None.
|
||||
|
||||
Combines the two real sources Hermes has — no estimation, ever:
|
||||
- ``agent.session_actual_cost_usd``: per-response ``usage.cost``
|
||||
accumulator (OpenRouter usage accounting).
|
||||
- Nous ``x-nous-credits-*`` header delta via
|
||||
``agent.get_credits_spent_micros()`` (account-level spend since the
|
||||
session first saw a header; clamped at 0 so a mid-session top-up
|
||||
doesn't render a negative cost).
|
||||
|
||||
Returns None when neither source has reported anything — callers must
|
||||
hide their cost display in that case rather than showing $0.00.
|
||||
"""
|
||||
total: Optional[float] = None
|
||||
|
||||
actual = _finite_nonneg_number(getattr(agent, "session_actual_cost_usd", None))
|
||||
if actual is not None:
|
||||
total = actual
|
||||
|
||||
try:
|
||||
spent_micros = agent.get_credits_spent_micros()
|
||||
except Exception:
|
||||
spent_micros = None
|
||||
if spent_micros is not None:
|
||||
try:
|
||||
spent_usd = max(0, int(spent_micros)) / 1_000_000
|
||||
except (TypeError, ValueError):
|
||||
spent_usd = None
|
||||
if spent_usd is not None:
|
||||
total = (total or 0.0) + spent_usd
|
||||
|
||||
return total
|
||||
|
||||
|
||||
def has_known_pricing(
|
||||
model_name: str,
|
||||
provider: Optional[str] = None,
|
||||
|
||||
@@ -1902,12 +1902,36 @@ function resolveWebDist() {
|
||||
const unpackedDist = path.join(unpackedPathFor(APP_ROOT), 'dist')
|
||||
if (directoryExists(unpackedDist)) return unpackedDist
|
||||
|
||||
return path.join(APP_ROOT, 'dist')
|
||||
// Final fallback: APP_ROOT/dist. When packaged with asar:true this lives
|
||||
// INSIDE app.asar — not a servable filesystem directory — so the embedded
|
||||
// dashboard backend 404s on static routes (see #41327, #39472). The durable
|
||||
// fix is unpacking dist/ (PR #41411 adds dist/** to asarUnpack so the tier-2
|
||||
// unpackedDist above resolves). If we still land here while packaged, log it
|
||||
// so the cause isn't silent.
|
||||
const fallback = path.join(APP_ROOT, 'dist')
|
||||
if (IS_PACKAGED && /app\.asar(?=$|[\\/])/.test(fallback) && !directoryExists(fallback)) {
|
||||
rememberLog(
|
||||
`[web-dist] dashboard frontend dir resolved to an asar-internal path that ` +
|
||||
`is not a real directory: ${fallback}. Static routes will 404. ` +
|
||||
`Ensure dist/** is unpacked (asarUnpack) or set HERMES_DESKTOP_WEB_DIST.`
|
||||
)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function resolveRendererIndex() {
|
||||
const candidates = [path.join(APP_ROOT, 'dist', 'index.html'), path.join(resolveWebDist(), 'index.html')]
|
||||
return candidates.find(fileExists) || candidates[0]
|
||||
const found = candidates.find(fileExists)
|
||||
if (found) return found
|
||||
// Nothing on disk. A packaged build with no renderer bundle blank-pages with
|
||||
// a bare ERR_FILE_NOT_FOUND and no clue why (see #39484). Surface the cause
|
||||
// and the fix before Electron loads the missing file.
|
||||
rememberLog(
|
||||
`[renderer] index.html not found — the desktop app was packaged without a ` +
|
||||
`renderer bundle. Tried: ${candidates.join(', ')}. ` +
|
||||
`Rebuild with: hermes desktop --force-build`
|
||||
)
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
function resolveHermesCwd() {
|
||||
@@ -3137,7 +3161,7 @@ function buildApplicationMenu() {
|
||||
label: 'Actual Size',
|
||||
accelerator: 'CommandOrControl+0',
|
||||
click: () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.setZoomLevel(0)
|
||||
setAndPersistZoomLevel(mainWindow, 0)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3145,8 +3169,7 @@ function buildApplicationMenu() {
|
||||
accelerator: 'CommandOrControl+Plus',
|
||||
click: () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const next = Math.min(mainWindow.webContents.getZoomLevel() + 0.1, 9)
|
||||
mainWindow.webContents.setZoomLevel(next)
|
||||
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() + 0.1)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3155,8 +3178,7 @@ function buildApplicationMenu() {
|
||||
accelerator: 'CommandOrControl+-',
|
||||
click: () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const next = Math.max(mainWindow.webContents.getZoomLevel() - 0.1, -9)
|
||||
mainWindow.webContents.setZoomLevel(next)
|
||||
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() - 0.1)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3218,6 +3240,38 @@ function installPreviewShortcut(window) {
|
||||
})
|
||||
}
|
||||
|
||||
// Zoom level is persisted in the renderer's own localStorage (per-origin,
|
||||
// survives reloads/restarts) rather than a main-process JSON file. The main
|
||||
// process owns setZoomLevel, so we mirror each change into localStorage and
|
||||
// read it back on did-finish-load to re-apply after reloads or crash recovery.
|
||||
const ZOOM_STORAGE_KEY = 'hermes:desktop:zoomLevel'
|
||||
|
||||
function clampZoomLevel(value) {
|
||||
if (!Number.isFinite(value)) return 0
|
||||
return Math.min(Math.max(value, -9), 9)
|
||||
}
|
||||
|
||||
function setAndPersistZoomLevel(window, zoomLevel) {
|
||||
if (!window || window.isDestroyed()) return
|
||||
const next = clampZoomLevel(zoomLevel)
|
||||
window.webContents.setZoomLevel(next)
|
||||
window.webContents
|
||||
.executeJavaScript(`try { localStorage.setItem(${JSON.stringify(ZOOM_STORAGE_KEY)}, ${JSON.stringify(String(next))}) } catch {}`)
|
||||
.catch(error => rememberLog(`[zoom] persist failed: ${error?.message || error}`))
|
||||
}
|
||||
|
||||
function restorePersistedZoomLevel(window) {
|
||||
if (!window || window.isDestroyed()) return
|
||||
window.webContents
|
||||
.executeJavaScript(`(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()`)
|
||||
.then(stored => {
|
||||
if (stored == null || !window || window.isDestroyed()) return
|
||||
const level = clampZoomLevel(Number(stored))
|
||||
window.webContents.setZoomLevel(level)
|
||||
})
|
||||
.catch(error => rememberLog(`[zoom] restore failed: ${error?.message || error}`))
|
||||
}
|
||||
|
||||
function installZoomShortcuts(window) {
|
||||
// Override Ctrl/Cmd + +/-/0 with half the default zoom step (0.1 vs 0.2).
|
||||
// The menu items handle this on macOS (where the menu is always present),
|
||||
@@ -3231,15 +3285,13 @@ function installZoomShortcuts(window) {
|
||||
const key = input.key
|
||||
if (key === '0') {
|
||||
event.preventDefault()
|
||||
window.webContents.setZoomLevel(0)
|
||||
setAndPersistZoomLevel(window, 0)
|
||||
} else if (key === '=' || key === '+') {
|
||||
event.preventDefault()
|
||||
const next = Math.min(window.webContents.getZoomLevel() + ZOOM_STEP, 9)
|
||||
window.webContents.setZoomLevel(next)
|
||||
setAndPersistZoomLevel(window, window.webContents.getZoomLevel() + ZOOM_STEP)
|
||||
} else if (key === '-') {
|
||||
event.preventDefault()
|
||||
const next = Math.max(window.webContents.getZoomLevel() - ZOOM_STEP, -9)
|
||||
window.webContents.setZoomLevel(next)
|
||||
setAndPersistZoomLevel(window, window.webContents.getZoomLevel() - ZOOM_STEP)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4614,7 +4666,7 @@ function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1220,
|
||||
height: 800,
|
||||
minWidth: 900,
|
||||
minWidth: 400,
|
||||
minHeight: 620,
|
||||
title: 'Hermes',
|
||||
// Frameless title bar on every platform so the renderer can paint the
|
||||
@@ -4730,6 +4782,7 @@ function createWindow() {
|
||||
}
|
||||
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
restorePersistedZoomLevel(mainWindow)
|
||||
broadcastBootProgress()
|
||||
sendWindowStateChanged()
|
||||
startHermes().catch(error => rememberLog(error.stack || error.message))
|
||||
@@ -4737,6 +4790,45 @@ function createWindow() {
|
||||
}
|
||||
|
||||
ipcMain.handle('hermes:connection', async (_event, profile) => ensureBackend(profile))
|
||||
// Reconnect-after-wake recovery. A REMOTE primary backend has no child process,
|
||||
// so the 'exit'/'error' handlers that would clear a dead connectionPromise never
|
||||
// fire — once the remote becomes unreachable across a sleep/wake the renderer
|
||||
// re-dials the same dead descriptor forever and the composer stays stuck on
|
||||
// "Starting Hermes…". Before the renderer's backoff loop reconnects, it asks us
|
||||
// to confirm the cached PRIMARY backend is still reachable; if a remote one is
|
||||
// not, we drop the cache so the next getConnection() rebuilds it. Local backends
|
||||
// self-heal via their child 'exit' handler, so we never touch them here.
|
||||
ipcMain.handle('hermes:connection:revalidate', async () => {
|
||||
if (!connectionPromise) {
|
||||
return { ok: true, rebuilt: false }
|
||||
}
|
||||
|
||||
let conn = null
|
||||
try {
|
||||
conn = await connectionPromise
|
||||
} catch {
|
||||
// The cached boot already rejected (its own catch nulls connectionPromise);
|
||||
// nothing to revalidate — the next getConnection() builds fresh.
|
||||
return { ok: true, rebuilt: false }
|
||||
}
|
||||
|
||||
if (!conn || conn.mode !== 'remote' || !conn.baseUrl) {
|
||||
return { ok: true, rebuilt: false }
|
||||
}
|
||||
|
||||
const base = conn.baseUrl.replace(/\/+$/, '')
|
||||
try {
|
||||
await fetchPublicJson(`${base}/api/status`, { timeoutMs: 2_500 })
|
||||
return { ok: true, rebuilt: false }
|
||||
} catch {
|
||||
// Unreachable remote: drop the stale cache so the renderer's next reconnect
|
||||
// tick rebuilds a fresh, reachable descriptor. resetHermesConnection only
|
||||
// nulls connectionPromise for a remote (no child to SIGTERM).
|
||||
rememberLog('Cached remote Hermes backend failed liveness probe; dropping stale connection.')
|
||||
resetHermesConnection()
|
||||
return { ok: true, rebuilt: true }
|
||||
}
|
||||
})
|
||||
ipcMain.handle('hermes:backend:touch', async (_event, profile) => {
|
||||
touchPoolBackend(profile)
|
||||
return { ok: true }
|
||||
|
||||
@@ -2,6 +2,7 @@ const { contextBridge, ipcRenderer, webUtils } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('hermesDesktop', {
|
||||
getConnection: profile => ipcRenderer.invoke('hermes:connection', profile),
|
||||
revalidateConnection: () => ipcRenderer.invoke('hermes:connection:revalidate'),
|
||||
touchBackend: profile => ipcRenderer.invoke('hermes:backend:touch', profile),
|
||||
getGatewayWsUrl: profile => ipcRenderer.invoke('hermes:gateway:ws-url', profile),
|
||||
getBootProgress: () => ipcRenderer.invoke('hermes:boot-progress:get'),
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"profile:main": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
||||
"profile:main:cpu": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"start": "npm run build && electron .",
|
||||
"build": "node scripts/assert-root-install.cjs && node scripts/write-build-stamp.cjs && node scripts/stage-native-deps.cjs && tsc -b && vite build",
|
||||
"build": "node scripts/assert-root-install.cjs && node scripts/write-build-stamp.cjs && node scripts/stage-native-deps.cjs && tsc -b && vite build && node scripts/assert-dist-built.cjs",
|
||||
"builder": "cross-env NODE_OPTIONS=--max-old-space-size=16384 electron-builder",
|
||||
"pack": "npm run build && npm run builder -- --dir",
|
||||
"dist": "npm run build && npm run builder",
|
||||
@@ -166,7 +166,8 @@
|
||||
"afterSign": "scripts/notarize.cjs",
|
||||
"asarUnpack": [
|
||||
"**/*.node",
|
||||
"**/prebuilds/**"
|
||||
"**/prebuilds/**",
|
||||
"dist/**"
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools",
|
||||
|
||||
BIN
apps/desktop/pr-assets/session-source-folders.png
Normal file
|
After Width: | Height: | Size: 770 KiB |
70
apps/desktop/scripts/assert-dist-built.cjs
Normal file
@@ -0,0 +1,70 @@
|
||||
"use strict"
|
||||
|
||||
// Build-time guard: refuse to hand a half-built renderer to electron-builder.
|
||||
//
|
||||
// `npm run pack` / `npm run dist*` are `npm run build && npm run builder`.
|
||||
// If the `build` step (tsc -b && vite build) fails but packaging proceeds
|
||||
// anyway — a stale checkout that fails typecheck, an interrupted vite build,
|
||||
// or npm not short-circuiting `&&` in some shells — electron-builder happily
|
||||
// packages an app with an empty or missing `dist/`. The result launches but
|
||||
// blank-pages with `ERR_FILE_NOT_FOUND` for dist/index.html, with no clue why.
|
||||
//
|
||||
// This runs at the tail of `build`, after vite build, so any packaging path
|
||||
// inherits it. It fails loud and early instead of shipping a broken bundle.
|
||||
// See issues #39484 (renderer blank page) and #41327 / #39472 (dashboard 404).
|
||||
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
// Pure check — returns { ok: true } or { ok: false, error: "..." }.
|
||||
// Kept side-effect-free so it can be unit tested without spawning a process.
|
||||
function checkDistBuilt(distDir) {
|
||||
if (!fs.existsSync(distDir) || !fs.statSync(distDir).isDirectory()) {
|
||||
return { ok: false, error: `no dist directory at ${distDir}` }
|
||||
}
|
||||
|
||||
const indexHtml = path.join(distDir, "index.html")
|
||||
if (!fs.existsSync(indexHtml) || !fs.statSync(indexHtml).isFile()) {
|
||||
return { ok: false, error: `dist/index.html is missing at ${indexHtml}` }
|
||||
}
|
||||
if (fs.statSync(indexHtml).size === 0) {
|
||||
return { ok: false, error: `dist/index.html is empty at ${indexHtml}` }
|
||||
}
|
||||
|
||||
// index.html alone isn't enough — vite emits hashed JS into dist/assets.
|
||||
// An index.html with no script bundle still blank-pages.
|
||||
const assetsDir = path.join(distDir, "assets")
|
||||
const hasAssets =
|
||||
fs.existsSync(assetsDir) &&
|
||||
fs.statSync(assetsDir).isDirectory() &&
|
||||
fs.readdirSync(assetsDir).some(name => name.endsWith(".js"))
|
||||
if (!hasAssets) {
|
||||
return { ok: false, error: `dist/assets has no built JS bundle (expected vite output under ${assetsDir})` }
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
function main() {
|
||||
const desktopRoot = path.resolve(__dirname, "..")
|
||||
const distDir = path.join(desktopRoot, "dist")
|
||||
const result = checkDistBuilt(distDir)
|
||||
|
||||
if (!result.ok) {
|
||||
console.error(`\n✗ assert-dist-built: ${result.error}`)
|
||||
console.error(" The renderer bundle is missing or incomplete, so packaging")
|
||||
console.error(" would produce an app that launches to a blank page.")
|
||||
console.error(" Re-run the build and check the tsc/vite output above for the")
|
||||
console.error(" real failure, then package again:")
|
||||
console.error(` cd ${desktopRoot} && npm run build\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("✓ assert-dist-built: dist/index.html + assets present")
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
|
||||
module.exports = { checkDistBuilt }
|
||||
84
apps/desktop/scripts/assert-dist-built.test.cjs
Normal file
@@ -0,0 +1,84 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
const test = require('node:test')
|
||||
|
||||
const { checkDistBuilt } = require('../scripts/assert-dist-built.cjs')
|
||||
|
||||
function makeDist(extra) {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-assert-dist-'))
|
||||
const distDir = path.join(tempRoot, 'dist')
|
||||
fs.mkdirSync(distDir, { recursive: true })
|
||||
if (extra) extra(distDir)
|
||||
return { tempRoot, distDir }
|
||||
}
|
||||
|
||||
test('checkDistBuilt passes when index.html + an assets JS bundle exist', () => {
|
||||
const { tempRoot, distDir } = makeDist(d => {
|
||||
fs.writeFileSync(path.join(d, 'index.html'), '<!doctype html><div id=root></div>', 'utf8')
|
||||
fs.mkdirSync(path.join(d, 'assets'))
|
||||
fs.writeFileSync(path.join(d, 'assets', 'index-abc123.js'), 'console.log(1)', 'utf8')
|
||||
})
|
||||
try {
|
||||
assert.deepEqual(checkDistBuilt(distDir), { ok: true })
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('checkDistBuilt fails when the dist directory is absent', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-assert-dist-'))
|
||||
try {
|
||||
const result = checkDistBuilt(path.join(tempRoot, 'dist'))
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(result.error, /no dist directory/)
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('checkDistBuilt fails when index.html is missing', () => {
|
||||
const { tempRoot, distDir } = makeDist(d => {
|
||||
fs.mkdirSync(path.join(d, 'assets'))
|
||||
fs.writeFileSync(path.join(d, 'assets', 'index-abc123.js'), 'console.log(1)', 'utf8')
|
||||
})
|
||||
try {
|
||||
const result = checkDistBuilt(distDir)
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(result.error, /index\.html is missing/)
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('checkDistBuilt fails when index.html is empty', () => {
|
||||
const { tempRoot, distDir } = makeDist(d => {
|
||||
fs.writeFileSync(path.join(d, 'index.html'), '', 'utf8')
|
||||
fs.mkdirSync(path.join(d, 'assets'))
|
||||
fs.writeFileSync(path.join(d, 'assets', 'index-abc123.js'), 'console.log(1)', 'utf8')
|
||||
})
|
||||
try {
|
||||
const result = checkDistBuilt(distDir)
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(result.error, /index\.html is empty/)
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('checkDistBuilt fails when assets/ has no JS bundle', () => {
|
||||
const { tempRoot, distDir } = makeDist(d => {
|
||||
fs.writeFileSync(path.join(d, 'index.html'), '<!doctype html>', 'utf8')
|
||||
fs.mkdirSync(path.join(d, 'assets'))
|
||||
// CSS only, no JS — still a blank page at runtime.
|
||||
fs.writeFileSync(path.join(d, 'assets', 'index-abc123.css'), 'body{}', 'utf8')
|
||||
})
|
||||
try {
|
||||
const result = checkDistBuilt(distDir)
|
||||
assert.equal(result.ok, false)
|
||||
assert.match(result.error, /no built JS bundle/)
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -124,7 +124,10 @@ function ChatHeader({
|
||||
|
||||
return (
|
||||
<header className={cn(titlebarHeaderBaseClass, isRoutedSessionView && titlebarHeaderShadowClass)}>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className="min-w-0 flex-1"
|
||||
style={{ maxWidth: 'calc(100vw - var(--titlebar-content-inset,0px) - var(--titlebar-tools-right) - var(--titlebar-tools-width) - 1.5rem)' }}
|
||||
>
|
||||
<SessionActionsMenu
|
||||
align="start"
|
||||
onDelete={selectedSessionId ? onDeleteSelectedSession : undefined}
|
||||
@@ -135,11 +138,11 @@ function ChatHeader({
|
||||
title={title}
|
||||
>
|
||||
<Button
|
||||
className="pointer-events-auto h-6 min-w-0 gap-1 border border-transparent bg-transparent px-2 py-0 text-(--ui-text-secondary) hover:border-(--ui-stroke-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground data-[state=open]:border-(--ui-stroke-tertiary) data-[state=open]:bg-(--ui-control-active-background) [-webkit-app-region:no-drag]"
|
||||
className="pointer-events-auto flex h-6 min-w-0 max-w-full gap-1 border border-transparent bg-transparent px-2 py-0 text-(--ui-text-secondary) hover:border-(--ui-stroke-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground data-[state=open]:border-(--ui-stroke-tertiary) data-[state=open]:bg-(--ui-control-active-background) [-webkit-app-region:no-drag]"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<h2 className="max-w-[52vw] truncate text-[0.75rem] font-medium leading-none">{title}</h2>
|
||||
<h2 className="min-w-0 flex-1 truncate text-[0.75rem] font-medium leading-none">{title}</h2>
|
||||
<Codicon className="shrink-0 text-(--ui-text-tertiary)" name="chevron-down" size="0.8125rem" />
|
||||
</Button>
|
||||
</SessionActionsMenu>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useStore } from '@nanostores/react'
|
||||
import type * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { PlatformAvatar } from '@/app/messaging/platform-icon'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
|
||||
@@ -39,6 +40,7 @@ import { searchSessions, type SessionInfo, type SessionSearchResult } from '@/he
|
||||
import { useI18n } from '@/i18n'
|
||||
import { profileColor } from '@/lib/profile-color'
|
||||
import { sessionMatchesSearch } from '@/lib/session-search'
|
||||
import { normalizeSessionSource, sessionSourceLabel } from '@/lib/session-source'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { $cronJobs } from '@/store/cron'
|
||||
import {
|
||||
@@ -47,8 +49,11 @@ import {
|
||||
$sidebarAgentsGrouped,
|
||||
$sidebarCronOpen,
|
||||
$sidebarOpen,
|
||||
$sidebarOverlayMounted,
|
||||
$sidebarPinsOpen,
|
||||
$sidebarRecentsOpen,
|
||||
$sidebarSessionOrderIds,
|
||||
$sidebarWorkspaceOrderIds,
|
||||
pinSession,
|
||||
reorderPinnedSession,
|
||||
SESSION_SEARCH_FOCUS_EVENT,
|
||||
@@ -56,6 +61,8 @@ import {
|
||||
setSidebarCronOpen,
|
||||
setSidebarPinsOpen,
|
||||
setSidebarRecentsOpen,
|
||||
setSidebarSessionOrderIds,
|
||||
setSidebarWorkspaceOrderIds,
|
||||
SIDEBAR_SESSIONS_PAGE_SIZE,
|
||||
unpinSession
|
||||
} from '@/store/layout'
|
||||
@@ -116,10 +123,14 @@ const WORKSPACE_PAGE = 5
|
||||
// ALL-profiles view: show only the latest N per profile up front to keep the
|
||||
// unified list scannable, then reveal/fetch more in N-sized steps on demand.
|
||||
const PROFILE_INITIAL_PAGE = 5
|
||||
const WS_ID_PREFIX = 'workspace:'
|
||||
const GROUP_DND_ID_PREFIX = 'group:'
|
||||
const LOCAL_SESSION_SOURCES = new Set(['cli', 'desktop', 'local', 'tui'])
|
||||
|
||||
const groupDndId = (id: string) => `${GROUP_DND_ID_PREFIX}${id}`
|
||||
|
||||
const parseGroupDndId = (id: string) =>
|
||||
id.startsWith(GROUP_DND_ID_PREFIX) ? id.slice(GROUP_DND_ID_PREFIX.length) : null
|
||||
|
||||
const wsId = (id: string) => `${WS_ID_PREFIX}${id}`
|
||||
const parseWsId = (id: string) => (id.startsWith(WS_ID_PREFIX) ? id.slice(WS_ID_PREFIX.length) : null)
|
||||
const countLabel = (loaded: number, total: number) => (total > loaded ? `${loaded}/${total}` : String(loaded))
|
||||
const sessionTime = (s: SessionInfo) => s.last_active || s.started_at || 0
|
||||
|
||||
@@ -150,6 +161,33 @@ function orderByIds<T>(items: T[], getId: (item: T) => string, orderIds: string[
|
||||
return out
|
||||
}
|
||||
|
||||
function reconcileOrderIds(currentIds: string[], orderIds: string[]): string[] {
|
||||
if (!currentIds.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!orderIds.length) {
|
||||
return currentIds
|
||||
}
|
||||
|
||||
const current = new Set(currentIds)
|
||||
const next = orderIds.filter(id => current.has(id))
|
||||
const known = new Set(next)
|
||||
|
||||
for (const id of currentIds) {
|
||||
if (!known.has(id)) {
|
||||
next.push(id)
|
||||
known.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
function sameIds(left: string[], right: string[]) {
|
||||
return left.length === right.length && left.every((item, index) => item === right[index])
|
||||
}
|
||||
|
||||
const baseName = (path: string) =>
|
||||
path
|
||||
.replace(/[/\\]+$/, '')
|
||||
@@ -183,7 +221,11 @@ function searchResultToSession(result: SessionSearchResult): SessionInfo {
|
||||
}
|
||||
}
|
||||
|
||||
function workspaceGroupsFor(sessions: SessionInfo[], noWorkspaceLabel: string): SidebarSessionGroup[] {
|
||||
function workspaceGroupsFor(
|
||||
sessions: SessionInfo[],
|
||||
noWorkspaceLabel: string,
|
||||
options: { preserveSessionOrder?: boolean } = {}
|
||||
): SidebarSessionGroup[] {
|
||||
const groups = new Map<string, SidebarSessionGroup>()
|
||||
|
||||
for (const session of sessions) {
|
||||
@@ -196,17 +238,56 @@ function workspaceGroupsFor(sessions: SessionInfo[], noWorkspaceLabel: string):
|
||||
groups.set(id, group)
|
||||
}
|
||||
|
||||
// Groups keep recency order (Map insertion = first-seen in the recency-sorted
|
||||
// input, so an active project floats up), but rows *within* a group sort by
|
||||
// creation time so they don't reshuffle every time a message lands — keeps
|
||||
// muscle memory intact.
|
||||
for (const group of groups.values()) {
|
||||
group.sessions.sort((a, b) => b.started_at - a.started_at)
|
||||
if (!options.preserveSessionOrder) {
|
||||
// Groups keep recency order (Map insertion = first-seen in the recency-sorted
|
||||
// input, so an active project floats up), but rows *within* a group sort by
|
||||
// creation time so they don't reshuffle every time a message lands — keeps
|
||||
// muscle memory intact.
|
||||
for (const group of groups.values()) {
|
||||
group.sessions.sort((a, b) => b.started_at - a.started_at)
|
||||
}
|
||||
}
|
||||
|
||||
return [...groups.values()]
|
||||
}
|
||||
|
||||
function sourceSessionGroupsFor(sessions: SessionInfo[]): {
|
||||
localSessions: SessionInfo[]
|
||||
sourceGroups: SidebarSessionGroup[]
|
||||
} {
|
||||
const groups = new Map<string, SidebarSessionGroup>()
|
||||
const localSessions: SessionInfo[] = []
|
||||
|
||||
for (const session of sessions) {
|
||||
const sourceId = normalizeSessionSource(session.source)
|
||||
|
||||
if (!sourceId || LOCAL_SESSION_SOURCES.has(sourceId)) {
|
||||
localSessions.push(session)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const label = sessionSourceLabel(sourceId) ?? sourceId
|
||||
|
||||
const group = groups.get(sourceId) ?? {
|
||||
id: `source:${sourceId}`,
|
||||
label,
|
||||
mode: 'source',
|
||||
path: null,
|
||||
sessions: [],
|
||||
sourceId
|
||||
}
|
||||
|
||||
group.sessions.push(session)
|
||||
groups.set(sourceId, group)
|
||||
}
|
||||
|
||||
return {
|
||||
localSessions,
|
||||
sourceGroups: [...groups.values()].sort((a, b) => sessionTime(b.sessions[0]) - sessionTime(a.sessions[0]))
|
||||
}
|
||||
}
|
||||
|
||||
function useSortableBindings(id: string) {
|
||||
const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({ id })
|
||||
|
||||
@@ -215,7 +296,11 @@ function useSortableBindings(id: string) {
|
||||
dragHandleProps: { ...attributes, ...listeners },
|
||||
ref: setNodeRef,
|
||||
reorderable: true as const,
|
||||
style: { transform: CSS.Transform.toString(transform), transition }
|
||||
style: {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: isDragging ? undefined : transition,
|
||||
willChange: isDragging ? 'transform' : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +332,9 @@ export function ChatSidebar({
|
||||
const { t } = useI18n()
|
||||
const s = t.sidebar
|
||||
const sidebarOpen = useStore($sidebarOpen)
|
||||
// Collapsed-but-overlay-mounted → render the full sidebar, not just the nav rail.
|
||||
const overlayMounted = useStore($sidebarOverlayMounted)
|
||||
const contentVisible = sidebarOpen || overlayMounted
|
||||
const panesFlipped = useStore($panesFlipped)
|
||||
const agentsGrouped = useStore($sidebarAgentsGrouped)
|
||||
const pinnedSessionIds = useStore($pinnedSessionIds)
|
||||
@@ -270,8 +358,8 @@ export function ChatSidebar({
|
||||
// profile while scope is still ALL (persisted), the rail is hidden and they'd
|
||||
// otherwise be stuck in the grouped view with no way out.
|
||||
const showAllProfiles = multiProfile && profileScope === ALL_PROFILES
|
||||
const [agentOrderIds, setAgentOrderIds] = useState<string[]>([])
|
||||
const [workspaceOrderIds, setWorkspaceOrderIds] = useState<string[]>([])
|
||||
const agentOrderIds = useStore($sidebarSessionOrderIds)
|
||||
const workspaceOrderIds = useStore($sidebarWorkspaceOrderIds)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [serverMatches, setServerMatches] = useState<SessionSearchResult[]>([])
|
||||
const [newSessionKbdFlash, setNewSessionKbdFlash] = useState(false)
|
||||
@@ -425,14 +513,40 @@ export function ChatSidebar({
|
||||
[sortedSessions, pinnedRealIdSet]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const next = reconcileOrderIds(
|
||||
unpinnedAgentSessions.map(s => s.id),
|
||||
agentOrderIds
|
||||
)
|
||||
|
||||
if (!sameIds(next, agentOrderIds)) {
|
||||
setSidebarSessionOrderIds(next)
|
||||
}
|
||||
}, [agentOrderIds, unpinnedAgentSessions])
|
||||
|
||||
const agentSessions = useMemo(
|
||||
() => orderByIds(unpinnedAgentSessions, s => s.id, agentOrderIds),
|
||||
[unpinnedAgentSessions, agentOrderIds]
|
||||
)
|
||||
|
||||
const { localSessions: localAgentSessions, sourceGroups } = useMemo(
|
||||
() => sourceSessionGroupsFor(agentSessions),
|
||||
[agentSessions]
|
||||
)
|
||||
|
||||
const orderedSourceGroups = useMemo(
|
||||
() => orderByIds(sourceGroups, g => g.id, workspaceOrderIds),
|
||||
[sourceGroups, workspaceOrderIds]
|
||||
)
|
||||
|
||||
const agentGroups = useMemo(
|
||||
() => orderByIds(workspaceGroupsFor(agentSessions, s.noWorkspace), g => g.id, workspaceOrderIds),
|
||||
[agentSessions, s.noWorkspace, workspaceOrderIds]
|
||||
() =>
|
||||
orderByIds(
|
||||
workspaceGroupsFor(localAgentSessions, s.noWorkspace, { preserveSessionOrder: sourceGroups.length > 0 }),
|
||||
g => g.id,
|
||||
workspaceOrderIds
|
||||
),
|
||||
[localAgentSessions, s.noWorkspace, sourceGroups.length, workspaceOrderIds]
|
||||
)
|
||||
|
||||
const loadMoreForProfileGroup = useCallback(
|
||||
@@ -445,9 +559,7 @@ export function ChatSidebar({
|
||||
|
||||
void Promise.resolve(onLoadMoreProfileSessions(profile))
|
||||
.catch(() => undefined)
|
||||
.finally(() =>
|
||||
setProfileLoadMorePending(({ [profile]: _done, ...rest }) => rest)
|
||||
)
|
||||
.finally(() => setProfileLoadMorePending(({ [profile]: _done, ...rest }) => rest))
|
||||
},
|
||||
[onLoadMoreProfileSessions]
|
||||
)
|
||||
@@ -478,15 +590,17 @@ export function ChatSidebar({
|
||||
groups.set(key, group)
|
||||
}
|
||||
|
||||
return [...groups.values()]
|
||||
.map(group => ({
|
||||
...group,
|
||||
loadingMore: Boolean(profileLoadMorePending[group.id]),
|
||||
onLoadMore: onLoadMoreProfileSessions ? () => loadMoreForProfileGroup(group.id) : undefined,
|
||||
totalCount: Math.max(group.sessions.length, sessionProfileTotals[group.id] ?? 0)
|
||||
}))
|
||||
// default (root) first, then the rest alphabetically.
|
||||
.sort((a, b) => (a.id === 'default' ? -1 : b.id === 'default' ? 1 : a.label.localeCompare(b.label)))
|
||||
return (
|
||||
[...groups.values()]
|
||||
.map(group => ({
|
||||
...group,
|
||||
loadingMore: Boolean(profileLoadMorePending[group.id]),
|
||||
onLoadMore: onLoadMoreProfileSessions ? () => loadMoreForProfileGroup(group.id) : undefined,
|
||||
totalCount: Math.max(group.sessions.length, sessionProfileTotals[group.id] ?? 0)
|
||||
}))
|
||||
// default (root) first, then the rest alphabetically.
|
||||
.sort((a, b) => (a.id === 'default' ? -1 : b.id === 'default' ? 1 : a.label.localeCompare(b.label)))
|
||||
)
|
||||
}, [
|
||||
showAllProfiles,
|
||||
agentSessions,
|
||||
@@ -496,6 +610,53 @@ export function ChatSidebar({
|
||||
sessionProfileTotals
|
||||
])
|
||||
|
||||
const displayAgentSessions = sourceGroups.length ? localAgentSessions : agentSessions
|
||||
|
||||
const displayAgentGroups = useMemo(() => {
|
||||
if (orderedSourceGroups.length) {
|
||||
const localGroups = agentsGrouped
|
||||
? agentGroups
|
||||
: localAgentSessions.length
|
||||
? [
|
||||
{
|
||||
id: 'local-sessions',
|
||||
label: 'Local',
|
||||
mode: 'workspace' as const,
|
||||
path: null,
|
||||
sessions: localAgentSessions
|
||||
}
|
||||
]
|
||||
: []
|
||||
|
||||
return orderByIds([...orderedSourceGroups, ...localGroups], g => g.id, workspaceOrderIds)
|
||||
}
|
||||
|
||||
return showAllProfiles ? profileGroups : agentsGrouped ? agentGroups : undefined
|
||||
}, [
|
||||
agentGroups,
|
||||
agentsGrouped,
|
||||
localAgentSessions,
|
||||
orderedSourceGroups,
|
||||
profileGroups,
|
||||
showAllProfiles,
|
||||
workspaceOrderIds
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!displayAgentGroups?.length || showAllProfiles) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = reconcileOrderIds(
|
||||
displayAgentGroups.map(g => g.id),
|
||||
workspaceOrderIds
|
||||
)
|
||||
|
||||
if (!sameIds(next, workspaceOrderIds)) {
|
||||
setSidebarWorkspaceOrderIds(next)
|
||||
}
|
||||
}, [displayAgentGroups, showAllProfiles, workspaceOrderIds])
|
||||
|
||||
const showSessionSkeletons = sessionsLoading && sortedSessions.length === 0
|
||||
|
||||
const showSessionSections = showSessionSkeletons || sortedSessions.length > 0
|
||||
@@ -543,23 +704,24 @@ export function ChatSidebar({
|
||||
|
||||
const activeId = String(active.id)
|
||||
const overId = String(over.id)
|
||||
const activeWs = parseWsId(activeId)
|
||||
const overWs = parseWsId(overId)
|
||||
const activeGroup = parseGroupDndId(activeId)
|
||||
const overGroup = parseGroupDndId(overId)
|
||||
|
||||
if (activeWs && overWs) {
|
||||
const oldIdx = agentGroups.findIndex(g => g.id === activeWs)
|
||||
const newIdx = agentGroups.findIndex(g => g.id === overWs)
|
||||
if (activeGroup && overGroup) {
|
||||
const groups = displayAgentGroups ?? []
|
||||
const oldIdx = groups.findIndex(g => g.id === activeGroup)
|
||||
const newIdx = groups.findIndex(g => g.id === overGroup)
|
||||
|
||||
if (oldIdx < 0 || newIdx < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setWorkspaceOrderIds(arrayMove(agentGroups, oldIdx, newIdx).map(g => g.id))
|
||||
setSidebarWorkspaceOrderIds(arrayMove(groups, oldIdx, newIdx).map(g => g.id))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (activeWs || overWs) {
|
||||
if (activeGroup || overGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -570,7 +732,7 @@ export function ChatSidebar({
|
||||
return
|
||||
}
|
||||
|
||||
setAgentOrderIds(arrayMove(agentSessions, oldIdx, newIdx).map(s => s.id))
|
||||
setSidebarSessionOrderIds(arrayMove(agentSessions, oldIdx, newIdx).map(s => s.id))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -580,7 +742,11 @@ export function ChatSidebar({
|
||||
panesFlipped ? 'border-l border-r-0' : 'border-r border-l-0',
|
||||
sidebarOpen
|
||||
? 'border-(--sidebar-edge-border) bg-(--ui-sidebar-surface-background) opacity-100'
|
||||
: 'pointer-events-none border-transparent bg-transparent opacity-0'
|
||||
: 'pointer-events-none border-transparent bg-transparent opacity-0',
|
||||
// While floated by PaneShell's hover-reveal, force visible + interactive
|
||||
// — on hover (group-hover/reveal) or when keyboard-pinned (data-forced).
|
||||
'in-data-[pane-hover-reveal=open]:pointer-events-auto in-data-[pane-hover-reveal=open]:border-(--sidebar-edge-border) in-data-[pane-hover-reveal=open]:bg-(--ui-sidebar-surface-background) in-data-[pane-hover-reveal=open]:opacity-100',
|
||||
'group-hover/reveal:pointer-events-auto group-hover/reveal:border-(--sidebar-edge-border) group-hover/reveal:bg-(--ui-sidebar-surface-background) group-hover/reveal:opacity-100'
|
||||
)}
|
||||
collapsible="none"
|
||||
>
|
||||
@@ -624,14 +790,14 @@ export function ChatSidebar({
|
||||
type="button"
|
||||
>
|
||||
<item.icon className="size-4 shrink-0 text-[color-mix(in_srgb,currentColor_72%,transparent)]" />
|
||||
{sidebarOpen && (
|
||||
{contentVisible && (
|
||||
<>
|
||||
<span className="min-w-0 flex-1 truncate max-[46.25rem]:hidden">
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{s.nav[item.id] ?? item.label}
|
||||
</span>
|
||||
{isNewSession && (
|
||||
<KbdGroup
|
||||
className={cn('ml-auto max-[46.25rem]:hidden', newSessionKbdFlash && 'opacity-100!')}
|
||||
className={cn('ml-auto', newSessionKbdFlash && 'opacity-100!')}
|
||||
keys={[...NEW_SESSION_KBD]}
|
||||
/>
|
||||
)}
|
||||
@@ -645,7 +811,7 @@ export function ChatSidebar({
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
{sidebarOpen && showSessionSections && (
|
||||
{contentVisible && showSessionSections && (
|
||||
<div className="shrink-0 px-2 pb-1 pt-1">
|
||||
<SearchField
|
||||
aria-label={s.searchAria}
|
||||
@@ -657,7 +823,7 @@ export function ChatSidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sidebarOpen && showSessionSections && trimmedQuery && (
|
||||
{contentVisible && showSessionSections && trimmedQuery && (
|
||||
<SidebarSessionsSection
|
||||
activeSessionId={activeSidebarSessionId}
|
||||
contentClassName="flex min-h-0 flex-1 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75"
|
||||
@@ -681,7 +847,7 @@ export function ChatSidebar({
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarOpen && showSessionSections && !trimmedQuery && (
|
||||
{contentVisible && showSessionSections && !trimmedQuery && (
|
||||
<SidebarSessionsSection
|
||||
activeSessionId={activeSidebarSessionId}
|
||||
contentClassName="flex min-h-10 shrink-0 flex-col gap-px rounded-lg pb-2 pt-1"
|
||||
@@ -703,7 +869,7 @@ export function ChatSidebar({
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarOpen && showSessionSections && !trimmedQuery && (
|
||||
{contentVisible && showSessionSections && !trimmedQuery && (
|
||||
<SidebarSessionsSection
|
||||
activeSessionId={activeSidebarSessionId}
|
||||
contentClassName={cn(
|
||||
@@ -727,7 +893,7 @@ export function ChatSidebar({
|
||||
) : null
|
||||
}
|
||||
forceEmptyState={showSessionSkeletons}
|
||||
groups={showAllProfiles ? profileGroups : agentsGrouped ? agentGroups : undefined}
|
||||
groups={displayAgentGroups}
|
||||
headerAction={
|
||||
// Always reserve the icon-xs (size-6) slot so the header keeps the
|
||||
// same height whether or not the toggle renders — otherwise the
|
||||
@@ -736,7 +902,7 @@ export function ChatSidebar({
|
||||
// the toggle does nothing, and it's irrelevant in the ALL-profiles
|
||||
// view (always grouped by profile), so hide the button (not the slot).
|
||||
<div className="grid size-6 shrink-0 place-items-center">
|
||||
{!showAllProfiles && agentSessions.length > 0 ? (
|
||||
{!showAllProfiles && localAgentSessions.length > 0 ? (
|
||||
<Tip label={agentsGrouped ? s.groupTitleGrouped : s.groupTitleUngrouped}>
|
||||
<Button
|
||||
aria-label={agentsGrouped ? s.groupAriaGrouped : s.groupAriaUngrouped}
|
||||
@@ -770,13 +936,13 @@ export function ChatSidebar({
|
||||
open={agentsOpen}
|
||||
pinned={false}
|
||||
rootClassName="min-h-0 flex-1 p-0"
|
||||
sessions={agentSessions}
|
||||
sessions={displayAgentSessions}
|
||||
sortable={!showAllProfiles && agentSessions.length > 1}
|
||||
workingSessionIdSet={workingSessionIdSet}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarOpen && !trimmedQuery && cronJobs.length > 0 && (
|
||||
{contentVisible && !trimmedQuery && cronJobs.length > 0 && (
|
||||
<SidebarCronJobsSection
|
||||
jobs={cronJobs}
|
||||
label={s.cronJobs}
|
||||
@@ -788,9 +954,9 @@ export function ChatSidebar({
|
||||
/>
|
||||
)}
|
||||
|
||||
{sidebarOpen && !showSessionSections && <div className="min-h-0 flex-1" />}
|
||||
{contentVisible && !showSessionSections && <div className="min-h-0 flex-1" />}
|
||||
|
||||
{sidebarOpen && (
|
||||
{contentVisible && (
|
||||
<div className="shrink-0 px-0.5 pb-1 pt-0.5">
|
||||
<ProfileRail />
|
||||
</div>
|
||||
@@ -872,8 +1038,9 @@ interface SidebarSessionGroup {
|
||||
// Profile color for the ALL-profiles view; absent for workspace groups.
|
||||
color?: null | string
|
||||
loadingMore?: boolean
|
||||
mode?: 'profile' | 'workspace'
|
||||
mode?: 'profile' | 'source' | 'workspace'
|
||||
onLoadMore?: () => void
|
||||
sourceId?: string
|
||||
totalCount?: number
|
||||
}
|
||||
|
||||
@@ -928,7 +1095,8 @@ function SidebarSessionsSection({
|
||||
onReorder,
|
||||
dndSensors
|
||||
}: SidebarSessionsSectionProps) {
|
||||
const showEmptyState = forceEmptyState || sessions.length === 0
|
||||
const hasGroupedSessions = Boolean(groups?.some(group => group.sessions.length > 0))
|
||||
const showEmptyState = forceEmptyState || (!hasGroupedSessions && sessions.length === 0)
|
||||
const dndActive = sortable && !!onReorder
|
||||
|
||||
const renderRow = (session: SessionInfo) => {
|
||||
@@ -961,12 +1129,25 @@ function SidebarSessionsSection({
|
||||
renderRows(items)
|
||||
)
|
||||
|
||||
const renderNestedSessionList = (items: SessionInfo[]) =>
|
||||
dndActive ? (
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={onReorder} sensors={dndSensors}>
|
||||
<SortableContext items={items.map(s => s.id)} strategy={verticalListSortingStrategy}>
|
||||
{renderRows(items)}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
renderRows(items)
|
||||
)
|
||||
|
||||
const flatVirtualized = !showEmptyState && !groups?.length && sessions.length >= VIRTUALIZE_THRESHOLD
|
||||
|
||||
let inner: React.ReactNode
|
||||
let bodyOwnsDndContext = dndActive && !showEmptyState
|
||||
|
||||
if (showEmptyState) {
|
||||
inner = emptyState
|
||||
bodyOwnsDndContext = false
|
||||
} else if (groups?.length) {
|
||||
const groupNodes = groups.map(group =>
|
||||
dndActive ? (
|
||||
@@ -974,7 +1155,7 @@ function SidebarSessionsSection({
|
||||
group={group}
|
||||
key={group.id}
|
||||
onNewSession={onNewSessionInWorkspace}
|
||||
renderRows={renderSessionList}
|
||||
renderRows={renderNestedSessionList}
|
||||
/>
|
||||
) : (
|
||||
<SidebarWorkspaceGroup
|
||||
@@ -987,12 +1168,15 @@ function SidebarSessionsSection({
|
||||
)
|
||||
|
||||
inner = dndActive ? (
|
||||
<SortableContext items={groups.map(g => wsId(g.id))} strategy={verticalListSortingStrategy}>
|
||||
{groupNodes}
|
||||
</SortableContext>
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={onReorder} sensors={dndSensors}>
|
||||
<SortableContext items={groups.map(g => groupDndId(g.id))} strategy={verticalListSortingStrategy}>
|
||||
{groupNodes}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
groupNodes
|
||||
)
|
||||
bodyOwnsDndContext = false
|
||||
} else if (flatVirtualized) {
|
||||
inner = (
|
||||
<VirtualSessionList
|
||||
@@ -1011,14 +1195,13 @@ function SidebarSessionsSection({
|
||||
inner = renderSessionList(sessions)
|
||||
}
|
||||
|
||||
const body =
|
||||
dndActive && !showEmptyState ? (
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={onReorder} sensors={dndSensors}>
|
||||
{inner}
|
||||
</DndContext>
|
||||
) : (
|
||||
inner
|
||||
)
|
||||
const body = bodyOwnsDndContext ? (
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={onReorder} sensors={dndSensors}>
|
||||
{inner}
|
||||
</DndContext>
|
||||
) : (
|
||||
inner
|
||||
)
|
||||
|
||||
// The virtualizer owns its own scroller, so suppress the wrapper's overflow
|
||||
// to avoid a double scroll container.
|
||||
@@ -1061,6 +1244,7 @@ function SidebarWorkspaceGroup({
|
||||
const { t } = useI18n()
|
||||
const s = t.sidebar
|
||||
const isProfileGroup = group.mode === 'profile'
|
||||
const isSourceGroup = group.mode === 'source'
|
||||
const pageStep = isProfileGroup ? PROFILE_INITIAL_PAGE : WORKSPACE_PAGE
|
||||
const [open, setOpen] = useState(true)
|
||||
const [visibleCount, setVisibleCount] = useState(pageStep)
|
||||
@@ -1086,7 +1270,16 @@ function SidebarWorkspaceGroup({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('grid gap-px', dragging && 'z-10 opacity-60', className)} ref={ref} style={style} {...rest}>
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-px data-[dragging=true]:z-10 data-[dragging=true]:opacity-70 data-[dragging=true]:will-change-transform',
|
||||
className
|
||||
)}
|
||||
data-dragging={dragging ? 'true' : undefined}
|
||||
ref={ref}
|
||||
style={style}
|
||||
{...rest}
|
||||
>
|
||||
<div className="group/workspace flex min-h-6 items-center gap-1 px-2 pt-1 text-[0.6875rem] font-medium text-(--ui-text-tertiary)">
|
||||
<button
|
||||
className="flex min-w-0 items-center gap-1.5 bg-transparent text-left hover:text-(--ui-text-secondary)"
|
||||
@@ -1094,7 +1287,18 @@ function SidebarWorkspaceGroup({
|
||||
type="button"
|
||||
>
|
||||
{group.color ? (
|
||||
<span aria-hidden="true" className="size-2 shrink-0 rounded-full" style={{ backgroundColor: group.color }} />
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="size-2 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: group.color }}
|
||||
/>
|
||||
) : null}
|
||||
{isSourceGroup && group.sourceId ? (
|
||||
<PlatformAvatar
|
||||
className="size-4 rounded-[4px] text-[0.5625rem] [&_svg]:size-3"
|
||||
platformId={group.sourceId}
|
||||
platformName={group.label}
|
||||
/>
|
||||
) : null}
|
||||
<span className="truncate">{group.label}</span>
|
||||
<SidebarCount>
|
||||
@@ -1143,7 +1347,11 @@ function SidebarWorkspaceGroup({
|
||||
{renderRows(visibleSessions)}
|
||||
{hiddenCount > 0 &&
|
||||
(isProfileGroup ? (
|
||||
<SidebarLoadMoreRow loading={Boolean(group.loadingMore)} onClick={handleProfileLoadMore} step={nextCount} />
|
||||
<SidebarLoadMoreRow
|
||||
loading={Boolean(group.loadingMore)}
|
||||
onClick={handleProfileLoadMore}
|
||||
step={nextCount}
|
||||
/>
|
||||
) : (
|
||||
<Tip label={s.showMoreIn(nextCount, group.label)}>
|
||||
<button
|
||||
@@ -1169,7 +1377,7 @@ interface SortableWorkspaceProps {
|
||||
}
|
||||
|
||||
function SortableSidebarWorkspaceGroup(props: SortableWorkspaceProps) {
|
||||
return <SidebarWorkspaceGroup {...props} {...useSortableBindings(wsId(props.group.id))} />
|
||||
return <SidebarWorkspaceGroup {...props} {...useSortableBindings(groupDndId(props.group.id))} />
|
||||
}
|
||||
|
||||
function SidebarCount({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -176,8 +176,8 @@ export function SidebarSessionRow({
|
||||
needsInput ? 'overflow-visible' : 'overflow-hidden'
|
||||
)}
|
||||
>
|
||||
<SidebarRowDot isWorking={isWorking} needsInput={needsInput} />
|
||||
</span>
|
||||
<SidebarRowDot isWorking={isWorking} needsInput={needsInput} />
|
||||
</span>
|
||||
)}
|
||||
<span className="min-w-0 flex-1 truncate text-[0.8125rem] font-normal text-(--ui-text-secondary) group-hover:text-foreground group-data-[working=true]:text-foreground/90">
|
||||
{title}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DesktopInstallOverlay } from '@/components/desktop-install-overlay'
|
||||
import { DesktopOnboardingOverlay } from '@/components/desktop-onboarding-overlay'
|
||||
import { GatewayConnectingOverlay } from '@/components/gateway-connecting-overlay'
|
||||
import { Pane, PaneMain } from '@/components/pane-shell'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
import { useSkinCommand } from '@/themes/use-skin-command'
|
||||
|
||||
import { formatRefValue } from '../components/assistant-ui/directive-text'
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
FILE_BROWSER_MAX_WIDTH,
|
||||
FILE_BROWSER_MIN_WIDTH,
|
||||
pinSession,
|
||||
setSidebarOverlayMounted,
|
||||
SIDEBAR_DEFAULT_WIDTH,
|
||||
SIDEBAR_MAX_WIDTH,
|
||||
SIDEBAR_SESSIONS_PAGE_SIZE,
|
||||
@@ -76,6 +78,7 @@ import { CommandPalette } from './command-palette'
|
||||
import { useGatewayBoot } from './gateway/hooks/use-gateway-boot'
|
||||
import { useGatewayRequest } from './gateway/hooks/use-gateway-request'
|
||||
import { useKeybinds } from './hooks/use-keybinds'
|
||||
import { SIDEBAR_COLLAPSE_MEDIA_QUERY } from './layout-constants'
|
||||
import { ModelPickerOverlay } from './model-picker-overlay'
|
||||
import { ModelVisibilityOverlay } from './model-visibility-overlay'
|
||||
import { RightSidebarPane } from './right-sidebar'
|
||||
@@ -165,6 +168,10 @@ export function DesktopController() {
|
||||
const terminalTakeover = useStore($terminalTakeover)
|
||||
const panesFlipped = useStore($panesFlipped)
|
||||
const profileScope = useStore($profileScope)
|
||||
// Below SIDEBAR_COLLAPSE_BREAKPOINT_PX there's no room for a docked rail —
|
||||
// collapse both sidebars (without touching their stored open state) so the
|
||||
// hover-reveal overlay becomes the way in. Restores once it's wide again.
|
||||
const narrowViewport = useMediaQuery(SIDEBAR_COLLAPSE_MEDIA_QUERY)
|
||||
|
||||
const routedSessionId = routeSessionId(location.pathname)
|
||||
const routeToken = `${location.pathname}:${location.search}:${location.hash}`
|
||||
@@ -300,6 +307,7 @@ export function DesktopController() {
|
||||
// with few recent sessions isn't windowed out of the cross-profile
|
||||
// recency page — the empty-history-on-profile-switch bug.
|
||||
const sessionProfile = profileScope === ALL_PROFILES ? 'all' : profileScope
|
||||
|
||||
const result = await listAllProfileSessions(limit, 1, 'exclude', 'recent', sessionProfile, {
|
||||
excludeSources: ['cron']
|
||||
})
|
||||
@@ -846,6 +854,8 @@ export function DesktopController() {
|
||||
<Pane
|
||||
defaultOpen={false}
|
||||
disabled={!chatOpen}
|
||||
forceCollapsed={narrowViewport}
|
||||
hoverReveal
|
||||
id="file-browser"
|
||||
key="file-browser"
|
||||
maxWidth={FILE_BROWSER_MAX_WIDTH}
|
||||
@@ -873,9 +883,12 @@ export function DesktopController() {
|
||||
>
|
||||
<Pane
|
||||
disabled={terminalTakeoverActive}
|
||||
forceCollapsed={narrowViewport}
|
||||
hoverReveal
|
||||
id="chat-sidebar"
|
||||
maxWidth={SIDEBAR_MAX_WIDTH}
|
||||
minWidth={SIDEBAR_DEFAULT_WIDTH}
|
||||
onOverlayActiveChange={setSidebarOverlayMounted}
|
||||
resizable
|
||||
side={sidebarSide}
|
||||
width={`${SIDEBAR_DEFAULT_WIDTH}px`}
|
||||
|
||||
@@ -120,6 +120,13 @@ export function useGatewayBoot({
|
||||
reconnecting = true
|
||||
|
||||
try {
|
||||
// Drop a stale REMOTE backend cache before re-dialing. After sleep/wake a
|
||||
// remote backend can become unreachable, but it has no child process
|
||||
// whose 'exit' would clear the main process's cached descriptor — without
|
||||
// this the renderer re-dials the same dead endpoint forever and stays on
|
||||
// "Starting Hermes…". The probe is a no-op for a healthy or local backend.
|
||||
await desktop.revalidateConnection?.().catch(() => undefined)
|
||||
|
||||
const conn = await desktop.getConnection($activeGatewayProfile.get())
|
||||
|
||||
if (cancelled) {
|
||||
@@ -218,6 +225,15 @@ export function useGatewayBoot({
|
||||
reconnectAttempt = 0
|
||||
reauthNotified = false
|
||||
clearReconnectTimer()
|
||||
|
||||
// A revalidate-driven reconnect can rebuild the backend in place when the
|
||||
// cached remote was found dead, which re-drives the boot-progress overlay.
|
||||
// Unlike the initial boot, nothing calls completeDesktopBoot() afterwards,
|
||||
// so dismiss it here once we're open again — otherwise the overlay sticks
|
||||
// at ~94%. A no-op on a normal (non-rebuild) reconnect.
|
||||
if (bootCompleted) {
|
||||
completeDesktopBoot()
|
||||
}
|
||||
} else if (bootCompleted && (st === 'closed' || st === 'error')) {
|
||||
// The socket dropped after a healthy boot (typically sleep/wake). Try
|
||||
// to bring it back instead of leaving the composer stuck disabled.
|
||||
|
||||
@@ -2,11 +2,15 @@ import { useEffect, useRef } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { setRightSidebarTab } from '@/app/right-sidebar/store'
|
||||
import { PANE_TOGGLE_REVEAL_EVENT } from '@/components/pane-shell'
|
||||
import { matchesQuery } from '@/hooks/use-media-query'
|
||||
import { PROFILE_SLOT_COUNT } from '@/lib/keybinds/actions'
|
||||
import { comboAllowedInInput, comboFromEvent, isEditableTarget } from '@/lib/keybinds/combo'
|
||||
import { toggleCommandPalette } from '@/store/command-palette'
|
||||
import { $capture, $comboIndex, endCapture, setBinding, toggleKeybindPanel } from '@/store/keybinds'
|
||||
import {
|
||||
CHAT_SIDEBAR_PANE_ID,
|
||||
FILE_BROWSER_PANE_ID,
|
||||
requestSessionSearchFocus,
|
||||
setFileBrowserOpen,
|
||||
toggleFileBrowserOpen,
|
||||
@@ -24,6 +28,7 @@ import { $activeSessionId, $sessions, setModelPickerOpen } from '@/store/session
|
||||
import { useTheme } from '@/themes/context'
|
||||
|
||||
import { requestComposerFocus } from '../chat/composer/focus'
|
||||
import { SIDEBAR_COLLAPSE_MEDIA_QUERY } from '../layout-constants'
|
||||
import {
|
||||
AGENTS_ROUTE,
|
||||
ARTIFACTS_ROUTE,
|
||||
@@ -109,8 +114,20 @@ export function useKeybinds(deps: KeybindRuntimeDeps): void {
|
||||
'session.focusSearch': requestSessionSearchFocus,
|
||||
'session.togglePin': deps.toggleSelectedPin,
|
||||
|
||||
'view.toggleSidebar': toggleSidebarOpen,
|
||||
'view.toggleRightSidebar': toggleFileBrowserOpen,
|
||||
'view.toggleSidebar': () => {
|
||||
if (matchesQuery(SIDEBAR_COLLAPSE_MEDIA_QUERY)) {
|
||||
window.dispatchEvent(new CustomEvent(PANE_TOGGLE_REVEAL_EVENT, { detail: { id: CHAT_SIDEBAR_PANE_ID } }))
|
||||
} else {
|
||||
toggleSidebarOpen()
|
||||
}
|
||||
},
|
||||
'view.toggleRightSidebar': () => {
|
||||
if (matchesQuery(SIDEBAR_COLLAPSE_MEDIA_QUERY)) {
|
||||
window.dispatchEvent(new CustomEvent(PANE_TOGGLE_REVEAL_EVENT, { detail: { id: FILE_BROWSER_PANE_ID } }))
|
||||
} else {
|
||||
toggleFileBrowserOpen()
|
||||
}
|
||||
},
|
||||
'view.showFiles': () => showRightSidebarTab('files'),
|
||||
'view.showTerminal': () => showRightSidebarTab('terminal'),
|
||||
'view.flipPanes': togglePanesFlipped,
|
||||
|
||||
@@ -11,3 +11,9 @@ export const PAGE_INSET_X = 'px-[clamp(1.25rem,4vw,4rem)]'
|
||||
// Matching negative inline-margin to bleed an element (e.g. a sticky header bar)
|
||||
// out to the gutter edges before re-applying PAGE_INSET_X.
|
||||
export const PAGE_INSET_NEG_X = '-mx-[clamp(1.25rem,4vw,4rem)]'
|
||||
|
||||
// Below this viewport width a docked sidebar leaves no room for content, so both
|
||||
// rails auto-collapse into the hover-reveal overlay. Single source of truth for
|
||||
// the responsive collapse point.
|
||||
export const SIDEBAR_COLLAPSE_BREAKPOINT_PX = 768
|
||||
export const SIDEBAR_COLLAPSE_MEDIA_QUERY = `(max-width: ${SIDEBAR_COLLAPSE_BREAKPOINT_PX}px)`
|
||||
|
||||
@@ -28,15 +28,17 @@ import { cn } from '@/lib/utils'
|
||||
type IconKind = 'brand' | 'generic'
|
||||
|
||||
interface PlatformIconSpec {
|
||||
Icon: ComponentType<SVGProps<SVGSVGElement>>
|
||||
Icon?: ComponentType<SVGProps<SVGSVGElement>>
|
||||
color: string
|
||||
kind: IconKind
|
||||
monogram?: string
|
||||
}
|
||||
|
||||
const PLATFORM_ICONS: Record<string, PlatformIconSpec> = {
|
||||
telegram: { Icon: SiTelegram, color: '#26A5E4', kind: 'brand' },
|
||||
discord: { Icon: SiDiscord, color: '#5865F2', kind: 'brand' },
|
||||
// Slack removed from Simple Icons by Salesforce request — letter monogram.
|
||||
slack: { color: '#4A154B', kind: 'brand', monogram: 'S' },
|
||||
mattermost: { Icon: SiMattermost, color: '#0058CC', kind: 'brand' },
|
||||
matrix: { Icon: SiMatrix, color: '#000000', kind: 'brand' },
|
||||
signal: { Icon: SiSignal, color: '#3A76F0', kind: 'brand' },
|
||||
@@ -87,7 +89,7 @@ export function PlatformAvatar({ className, platformId, platformName }: Platform
|
||||
color
|
||||
}}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
{Icon ? <Icon className="size-3.5" /> : spec.monogram || platformName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,28 @@ import { $busy, $messages, noteSessionActivity, setSessionAttention, setSessionW
|
||||
|
||||
import type { ClientSessionState } from '../../types'
|
||||
|
||||
// Shallow per-message identity check. When a flush carries no transcript
|
||||
// changes, `preserveLocalAssistantErrors` returns the same message objects in
|
||||
// the same order, so reference equality per slot is enough to detect "nothing
|
||||
// to publish" and avoid a needless `$messages` churn.
|
||||
function sameMessageList(a: ChatMessage[], b: ChatMessage[]): boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let index = 0; index < a.length; index += 1) {
|
||||
if (a[index] !== b[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
interface SessionStateCacheOptions {
|
||||
activeSessionId: string | null
|
||||
busyRef: MutableRefObject<boolean>
|
||||
@@ -88,7 +110,20 @@ export function useSessionStateCache({
|
||||
return
|
||||
}
|
||||
|
||||
setMessages(preserveLocalAssistantErrors(pending.state.messages, $messages.get()))
|
||||
// `preserveLocalAssistantErrors` always returns a fresh array, so publishing
|
||||
// it unconditionally puts a new `$messages` reference on the store every
|
||||
// flush — including the periodic `session.info` heartbeats that don't touch
|
||||
// the transcript. That churns ChatView → runtimeMessageRepository → the
|
||||
// assistant-ui runtime → the virtualizer, which re-measures and visibly
|
||||
// jerks the scroll position while the user is reading. Skip the publish when
|
||||
// the merged result is content-identical to what's already on screen.
|
||||
const currentMessages = $messages.get()
|
||||
const nextMessages = preserveLocalAssistantErrors(pending.state.messages, currentMessages)
|
||||
|
||||
if (!sameMessageList(nextMessages, currentMessages)) {
|
||||
setMessages(nextMessages)
|
||||
}
|
||||
|
||||
setBusy(pending.state.busy)
|
||||
setMutableRef(busyRef, pending.state.busy)
|
||||
setAwaitingResponse(pending.state.awaitingResponse)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useSyncExternalStore } from 'react'
|
||||
import { NotificationStack } from '@/components/notifications'
|
||||
import { PaneShell } from '@/components/pane-shell'
|
||||
import { SidebarProvider } from '@/components/ui/sidebar'
|
||||
import { useMediaQuery } from '@/hooks/use-media-query'
|
||||
import {
|
||||
$fileBrowserOpen,
|
||||
$panesFlipped,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
import { $paneWidthOverride } from '@/store/panes'
|
||||
import { $connection } from '@/store/session'
|
||||
|
||||
import { SIDEBAR_COLLAPSE_MEDIA_QUERY } from '../layout-constants'
|
||||
|
||||
import { KeybindPanel } from './keybind-panel'
|
||||
import { StatusbarControls, type StatusbarItem } from './statusbar-controls'
|
||||
import { TITLEBAR_HEIGHT, titlebarControlsPosition } from './titlebar'
|
||||
@@ -58,6 +61,7 @@ export function AppShell({
|
||||
const sidebarOpen = useStore($sidebarOpen)
|
||||
const fileBrowserOpen = useStore($fileBrowserOpen)
|
||||
const panesFlipped = useStore($panesFlipped)
|
||||
const narrowViewport = useMediaQuery(SIDEBAR_COLLAPSE_MEDIA_QUERY)
|
||||
const fileBrowserWidthOverride = useStore($paneWidthOverride(FILE_BROWSER_PANE_ID))
|
||||
const connection = useStore($connection)
|
||||
const viewportFullscreen = useSyncExternalStore(subscribeWindowSize, viewportIsFullscreen, () => false)
|
||||
@@ -71,8 +75,10 @@ export function AppShell({
|
||||
|
||||
// The inset clears the top-left titlebar buttons when nothing covers the
|
||||
// window's left edge. Default layout: the sessions sidebar sits there.
|
||||
// Flipped layout: the file browser does instead.
|
||||
const leftEdgePaneOpen = panesFlipped ? fileBrowserOpen : sidebarOpen
|
||||
// Flipped layout: the file browser does instead. Below the collapse
|
||||
// breakpoint both rails are force-collapsed (hover-reveal overlay), so the
|
||||
// edge is uncovered regardless of their stored open state.
|
||||
const leftEdgePaneOpen = !narrowViewport && (panesFlipped ? fileBrowserOpen : sidebarOpen)
|
||||
|
||||
const titlebarContentInset = leftEdgePaneOpen
|
||||
? 0
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react'
|
||||
|
||||
import type { CommandCenterSection } from '@/app/command-center'
|
||||
import { GatewayMenuPanel } from '@/app/shell/gateway-menu-panel'
|
||||
import { useI18n } from '@/i18n'
|
||||
import {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
@@ -16,12 +17,11 @@ import {
|
||||
Zap,
|
||||
ZapFilled
|
||||
} from '@/lib/icons'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { formatModelStatusLabel } from '@/lib/model-status-label'
|
||||
import type { RuntimeReadinessResult } from '@/lib/runtime-readiness'
|
||||
import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { setSessionYolo } from '@/lib/yolo-session'
|
||||
import { setGlobalYolo, setSessionYolo } from '@/lib/yolo-session'
|
||||
import { $desktopActionTasks } from '@/store/activity'
|
||||
import { $previewServerRestartStatus } from '@/store/preview'
|
||||
import {
|
||||
@@ -44,7 +44,7 @@ import { $desktopVersion, $updateApply, $updateStatus, setUpdateOverlayOpen } fr
|
||||
import type { StatusResponse } from '@/types/hermes'
|
||||
|
||||
import { CRON_ROUTE } from '../../routes'
|
||||
import type { StatusbarItem } from '../statusbar-controls'
|
||||
import type { StatusbarItem, StatusbarSelectModifiers } from '../statusbar-controls'
|
||||
|
||||
interface StatusbarItemsOptions {
|
||||
agentsOpen: boolean
|
||||
@@ -105,22 +105,39 @@ export function useStatusbarItems({
|
||||
// Per-session approval bypass (same scope as the TUI's Shift+Tab). On a
|
||||
// new-chat draft (no runtime session yet) we arm locally; the session-create
|
||||
// path applies it once the backend session exists.
|
||||
const toggleYolo = useCallback(async () => {
|
||||
const next = !$yoloActive.get()
|
||||
const sid = $activeSessionId.get()
|
||||
//
|
||||
// Shift+click flips the GLOBAL approvals.mode instead — a persistent,
|
||||
// all-sessions/CLI/TUI/cron bypass that survives restarts.
|
||||
const toggleYolo = useCallback(
|
||||
async (modifiers?: StatusbarSelectModifiers) => {
|
||||
const next = !$yoloActive.get()
|
||||
|
||||
setYoloActive(next)
|
||||
setYoloActive(next)
|
||||
|
||||
if (!sid) {
|
||||
return
|
||||
}
|
||||
if (modifiers?.shiftKey) {
|
||||
try {
|
||||
await setGlobalYolo(requestGateway, next)
|
||||
} catch {
|
||||
setYoloActive(!next)
|
||||
}
|
||||
|
||||
try {
|
||||
await setSessionYolo(requestGateway, sid, next)
|
||||
} catch {
|
||||
setYoloActive(!next)
|
||||
}
|
||||
}, [requestGateway])
|
||||
return
|
||||
}
|
||||
|
||||
const sid = $activeSessionId.get()
|
||||
|
||||
if (!sid) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await setSessionYolo(requestGateway, sid, next)
|
||||
} catch {
|
||||
setYoloActive(!next)
|
||||
}
|
||||
},
|
||||
[requestGateway]
|
||||
)
|
||||
|
||||
const showYoloToggle = gatewayState === 'open' && (!!activeSessionId || freshDraftReady)
|
||||
|
||||
@@ -333,7 +350,7 @@ export function useStatusbarItems({
|
||||
<Zap className="size-3.5 shrink-0 opacity-70" />
|
||||
),
|
||||
id: 'yolo',
|
||||
onSelect: () => void toggleYolo(),
|
||||
onSelect: modifiers => void toggleYolo(modifiers),
|
||||
title: yoloActive ? copy.yoloOn : copy.yoloOff,
|
||||
variant: 'action'
|
||||
},
|
||||
|
||||
@@ -35,12 +35,16 @@ export interface StatusbarItem {
|
||||
menuClassName?: string
|
||||
menuContent?: ReactNode
|
||||
menuItems?: readonly StatusbarMenuItem[]
|
||||
onSelect?: () => void
|
||||
onSelect?: (modifiers: StatusbarSelectModifiers) => void
|
||||
title?: string
|
||||
to?: string
|
||||
variant?: 'action' | 'link' | 'menu' | 'text'
|
||||
}
|
||||
|
||||
export interface StatusbarSelectModifiers {
|
||||
shiftKey: boolean
|
||||
}
|
||||
|
||||
export type StatusbarItemSide = 'left' | 'right'
|
||||
export type SetStatusbarItemGroup = (id: string, items: readonly StatusbarItem[], side?: StatusbarItemSide) => void
|
||||
|
||||
@@ -170,12 +174,12 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate:
|
||||
<button
|
||||
className={cn(STATUSBAR_ACTION_CLASS, item.className)}
|
||||
disabled={item.disabled}
|
||||
onClick={() => {
|
||||
onClick={event => {
|
||||
if (item.to) {
|
||||
navigate(item.to)
|
||||
}
|
||||
|
||||
item.onSelect?.()
|
||||
item.onSelect?.({ shiftKey: event.shiftKey })
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -425,7 +425,7 @@ function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTex
|
||||
<div className="aui-md-table my-2 max-w-full overflow-x-auto rounded-[0.375rem] border border-border">
|
||||
<table
|
||||
className={cn(
|
||||
'm-0 w-full border-collapse text-[0.8125rem] [&_tr]:border-b [&_tr]:border-border last:[&_tr]:border-0',
|
||||
'm-0 w-full min-w-[18rem] border-collapse text-[0.8125rem] [&_tr]:border-b [&_tr]:border-border last:[&_tr]:border-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -438,7 +438,7 @@ function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTex
|
||||
th: ({ className, ...props }: ComponentProps<'th'>) => (
|
||||
<th
|
||||
className={cn(
|
||||
'px-2.5 py-1.5 text-left align-middle text-[0.75rem] font-medium text-muted-foreground',
|
||||
'whitespace-nowrap px-2.5 py-1.5 text-left align-middle text-[0.75rem] font-medium text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -489,7 +489,7 @@ describe('assistant-ui streaming renderer', () => {
|
||||
expect(viewport.scrollTop).toBe(420)
|
||||
})
|
||||
|
||||
it('keeps sticky-bottom armed through viewport height changes during streaming', async () => {
|
||||
it('does not follow streaming content growth even while parked at the bottom', async () => {
|
||||
const { container } = render(<StreamingHarness />)
|
||||
|
||||
const content = container.querySelector('[data-slot="aui_thread-content"]') as HTMLDivElement
|
||||
@@ -508,6 +508,7 @@ describe('assistant-ui streaming renderer', () => {
|
||||
|
||||
await wait(80)
|
||||
|
||||
// Park the user at the bottom of the current content.
|
||||
await act(async () => {
|
||||
viewport.scrollTop = 800
|
||||
fireEvent.scroll(viewport)
|
||||
@@ -520,6 +521,9 @@ describe('assistant-ui streaming renderer', () => {
|
||||
fireEvent.scroll(viewport)
|
||||
})
|
||||
|
||||
// Content grows as tokens stream in. Streaming auto-follow is removed, so
|
||||
// the viewport must NOT chase the new bottom — it stays where the user
|
||||
// last left it.
|
||||
scrollHeight = 1_200
|
||||
|
||||
await act(async () => {
|
||||
@@ -529,7 +533,7 @@ describe('assistant-ui streaming renderer', () => {
|
||||
})
|
||||
await wait(0)
|
||||
|
||||
expect(viewport.scrollTop).toBe(1_200)
|
||||
expect(viewport.scrollTop).toBe(760)
|
||||
})
|
||||
|
||||
it('honors the first upward wheel scroll even when a programmatic bottom-pin scroll event is still pending', async () => {
|
||||
@@ -566,7 +570,7 @@ describe('assistant-ui streaming renderer', () => {
|
||||
expect(viewport.scrollTop).toBe(420)
|
||||
})
|
||||
|
||||
it('keeps following final code-highlight growth when a run completes at bottom', async () => {
|
||||
it('does not snap to the bottom on final code-highlight growth after a run completes', async () => {
|
||||
const { container } = render(<StreamingHarness />)
|
||||
|
||||
const content = container.querySelector('[data-slot="aui_thread-content"]') as HTMLDivElement
|
||||
@@ -588,10 +592,13 @@ describe('assistant-ui streaming renderer', () => {
|
||||
|
||||
await wait(650)
|
||||
|
||||
// Completion re-measures (Shiki highlight) and grows the content. The
|
||||
// post-run bottom lock is removed, so the viewport stays put instead of
|
||||
// snapping to the new bottom.
|
||||
scrollHeight = 1_700
|
||||
await wait(0)
|
||||
|
||||
expect(viewport.scrollTop).toBe(1_700)
|
||||
expect(viewport.scrollTop).toBe(800)
|
||||
})
|
||||
|
||||
it('does not restart bottom-follow after completion when the user scrolled up', async () => {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { setThreadScrolledUp } from '@/store/thread-scroll'
|
||||
const ESTIMATED_ITEM_HEIGHT = 220
|
||||
const OVERSCAN = 4
|
||||
const AT_BOTTOM_THRESHOLD = 4
|
||||
const POST_RUN_BOTTOM_LOCK_MS = 1_200
|
||||
|
||||
type ThreadMessageComponents = ComponentProps<typeof ThreadPrimitive.MessageByIndex>['components']
|
||||
|
||||
@@ -265,8 +264,27 @@ function useThreadScrollAnchor({
|
||||
return
|
||||
}
|
||||
|
||||
// Already parked at the bottom: writing `scrollTop` is a no-op and the
|
||||
// browser fires NO scroll event, so arming the programmatic gate here would
|
||||
// leave it permanently set. Repeated pins (streaming heartbeats, the
|
||||
// post-run lock loop) then accumulate the gate, and the next genuine user
|
||||
// scroll-up is misread as one of our programmatic scrolls — re-arming
|
||||
// sticky-bottom and yanking the viewport back down. Refresh trackers, bail.
|
||||
const distFromBottom = el.scrollHeight - (el.scrollTop + el.clientHeight)
|
||||
|
||||
if (distFromBottom <= AT_BOTTOM_THRESHOLD) {
|
||||
lastTopRef.current = el.scrollTop
|
||||
lastHeightRef.current = el.scrollHeight
|
||||
lastClientHeightRef.current = el.clientHeight
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Hold the disarm gate across the scroll event the next line will fire.
|
||||
programmaticScrollPendingRef.current += 1
|
||||
// Set to 1 rather than incrementing: coalesced writes within a frame fire a
|
||||
// single scroll event, so a counter > 1 can never drain and would swallow a
|
||||
// later real user scroll.
|
||||
programmaticScrollPendingRef.current = 1
|
||||
scrollElementToBottom(el)
|
||||
lastTopRef.current = el.scrollTop
|
||||
lastHeightRef.current = el.scrollHeight
|
||||
@@ -369,51 +387,15 @@ function useThreadScrollAnchor({
|
||||
}
|
||||
}, [scrollerRef, stickyBottomRef])
|
||||
|
||||
// Follow content growth (streaming, item measurements, loading indicator)
|
||||
// while armed. During fast streaming the ResizeObserver can fire many
|
||||
// times per frame as Streamdown re-tokenizes; coalesce to one pin per
|
||||
// animation frame so we don't run the scroll-event/re-pin chain
|
||||
// (~20+ ms self in `Virtualizer.getMaxScrollOffset`) several times per
|
||||
// token.
|
||||
useEffect(() => {
|
||||
if (!enabled || !isRunning) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const el = scrollerRef.current
|
||||
|
||||
if (!el) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let pinRafScheduled = false
|
||||
|
||||
const schedulePin = () => {
|
||||
if (pinRafScheduled || !stickyBottomRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
pinRafScheduled = true
|
||||
requestAnimationFrame(() => {
|
||||
pinRafScheduled = false
|
||||
|
||||
if (stickyBottomRef.current) {
|
||||
pinToBottom()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(schedulePin)
|
||||
|
||||
// Observe ONLY the content (firstElementChild), not the scroller `el`
|
||||
// itself. Resizes of the viewport/scroller (window resize, devtools
|
||||
// panel toggle) shouldn't trigger a pin — only content growth should.
|
||||
if (el.firstElementChild) {
|
||||
observer.observe(el.firstElementChild)
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [enabled, isRunning, pinToBottom, scrollerRef, stickyBottomRef])
|
||||
// Intentionally NO streaming auto-follow. Earlier builds ran a
|
||||
// ResizeObserver here that re-pinned the viewport to the bottom on every
|
||||
// content growth while a turn was running, so the chat tracked tokens as
|
||||
// they streamed. That behavior is removed by request: once a turn is in
|
||||
// flight the viewport stays exactly where the user left it. The viewport
|
||||
// is still moved to the bottom ONCE per user submit / new turn / session
|
||||
// change (see the layout effect and the session-change effect below) so a
|
||||
// freshly submitted message lands in view — but it does not chase the
|
||||
// stream afterward.
|
||||
|
||||
// Jump to bottom on session change OR when an empty thread first gets
|
||||
// content. Both share the same intent and the same effect.
|
||||
@@ -429,22 +411,21 @@ function useThreadScrollAnchor({
|
||||
}
|
||||
}, [enabled, groupCount, jumpToBottom, sessionKey])
|
||||
|
||||
// Pre-paint pin: when groupCount increases while armed (optimistic user
|
||||
// message insert, streaming assistant turn arriving, etc.), pin BEFORE
|
||||
// the browser commits the layout to screen. Using useLayoutEffect rather
|
||||
// than useEffect so this runs synchronously after React commits the DOM
|
||||
// mutation but before the browser paints. Without this, there's a ~50ms
|
||||
// visual window where the new message sits below the fold while we wait
|
||||
// for the ResizeObserver / scroll event chain to fire and re-pin.
|
||||
// Pre-paint pin: when groupCount increases while armed (a new turn arriving
|
||||
// from the user submit or assistant turn start), pin BEFORE the browser
|
||||
// commits the layout to screen. Using useLayoutEffect rather than useEffect
|
||||
// so this runs synchronously after React commits the DOM mutation but before
|
||||
// the browser paints. Without this, there's a ~50ms visual window where the
|
||||
// new message sits below the fold.
|
||||
//
|
||||
// We pin TWICE in this critical path — once synchronously, then once on
|
||||
// the next rAF. The second pin catches the case where React mounts the
|
||||
// new message in the second commit (after our layout effect ran), which
|
||||
// grows scrollHeight again; without the rAF pin the user briefly sees a
|
||||
// ~15 px gap below the new message until the RO catches up. Streaming
|
||||
// tokens use the rate-limited RO path only; only the group-count change
|
||||
// (which fires once per user submit / new turn arrival) pays for the
|
||||
// extra pin.
|
||||
// ~15 px gap below the new message. This fires once per user submit / new
|
||||
// turn arrival — it is NOT streaming-token follow (that path is removed
|
||||
// above), so a turn that streams a long response after this initial jump
|
||||
// will not chase the bottom.
|
||||
const prevGroupCountForLayoutRef = useRef(groupCount)
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled) {
|
||||
@@ -468,45 +449,17 @@ function useThreadScrollAnchor({
|
||||
prevGroupCountForLayoutRef.current = groupCount
|
||||
}, [enabled, groupCount, pinToBottom, stickyBottomRef])
|
||||
|
||||
// Completion swaps streaming placeholders/plain code for final rendered DOM
|
||||
// (notably Shiki-highlighted code). Keep following the bottom briefly after
|
||||
// `isRunning` flips false so that final measurement pass cannot strand the
|
||||
// viewport near the top of a large code block.
|
||||
// Intentionally NO post-run bottom lock. Earlier builds kept pinning to
|
||||
// the bottom for POST_RUN_BOTTOM_LOCK_MS after `isRunning` flipped false to
|
||||
// chase final Shiki re-highlight measurement. With streaming follow gone,
|
||||
// re-pinning at completion would yank the viewport back to the bottom even
|
||||
// though the user is reading earlier content — the opposite of what's
|
||||
// wanted. The one-time submit / new-turn jump already covers landing a
|
||||
// fresh message in view.
|
||||
const prevIsRunningForLayoutRef = useRef(isRunning)
|
||||
useLayoutEffect(() => {
|
||||
const finishedRun = prevIsRunningForLayoutRef.current && !isRunning
|
||||
prevIsRunningForLayoutRef.current = isRunning
|
||||
|
||||
if (!enabled || !finishedRun || !stickyBottomRef.current) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const lockUntil = performance.now() + POST_RUN_BOTTOM_LOCK_MS
|
||||
let lockRaf: number | null = null
|
||||
|
||||
const lockFrame = () => {
|
||||
lockRaf = null
|
||||
|
||||
if (!stickyBottomRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
pinToBottom()
|
||||
|
||||
if (performance.now() < lockUntil) {
|
||||
lockRaf = requestAnimationFrame(lockFrame)
|
||||
}
|
||||
}
|
||||
|
||||
pinToBottom()
|
||||
lockRaf = requestAnimationFrame(lockFrame)
|
||||
|
||||
return () => {
|
||||
if (lockRaf !== null) {
|
||||
cancelAnimationFrame(lockRaf)
|
||||
}
|
||||
}
|
||||
}, [enabled, isRunning, pinToBottom, stickyBottomRef])
|
||||
}, [isRunning])
|
||||
|
||||
useAuiEvent('thread.runStart', jumpToBottom)
|
||||
}
|
||||
|
||||
@@ -150,10 +150,7 @@ export const Thread: FC<{
|
||||
)
|
||||
|
||||
const emptyPlaceholder = intro ? (
|
||||
<div
|
||||
className="flex min-h-0 w-full flex-col items-center justify-center"
|
||||
style={{ paddingBottom: 'var(--composer-measured-height)' }}
|
||||
>
|
||||
<div className="flex min-h-0 w-full flex-col items-center justify-center pt-[var(--composer-measured-height)]">
|
||||
<Intro {...intro} />
|
||||
</div>
|
||||
) : undefined
|
||||
@@ -470,9 +467,7 @@ const ReasoningAccordionGroup: FC<{ children?: ReactNode; endIndex: number; star
|
||||
s =>
|
||||
s.thread.isRunning &&
|
||||
s.message.status?.type === 'running' &&
|
||||
s.message.parts
|
||||
.slice(Math.max(0, startIndex))
|
||||
.some(p => p?.type === 'reasoning' && p.status?.type !== 'complete')
|
||||
s.message.parts.slice(Math.max(0, startIndex)).some(p => p?.type === 'reasoning' && p.status?.type !== 'complete')
|
||||
)
|
||||
|
||||
// A reasoning group with no actual text is pure noise — drop the whole
|
||||
|
||||
@@ -160,14 +160,14 @@ export function Intro({ personality, seed }: IntroProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none flex w-full min-w-0 flex-col items-center justify-center px-3 py-6 text-center text-muted-foreground sm:px-6 lg:px-8"
|
||||
className="pointer-events-none flex w-full min-w-0 flex-col items-center justify-center px-0.5 py-6 text-center text-muted-foreground sm:px-6 lg:px-8"
|
||||
data-slot="aui_intro"
|
||||
>
|
||||
<div className="w-full min-w-0">
|
||||
<p
|
||||
aria-label={WORDMARK}
|
||||
className="fit-text mx-auto mb-3 w-[88%] font-['Collapse'] font-bold uppercase leading-[0.9] tracking-[0.08em] text-midground mix-blend-plus-lighter dark:text-foreground/90"
|
||||
style={{ '--fit-text-line-height': '0.9', '--fit-text-min': '2.75rem' } as CSSProperties}
|
||||
className="fit-text mx-auto mb-1 w-[calc(100%-1rem)] font-['Collapse'] font-bold uppercase leading-[0.9] tracking-[0.08em] text-midground mix-blend-plus-lighter dark:text-foreground/90"
|
||||
style={{ '--fit-min': '2.75rem' } as CSSProperties}
|
||||
>
|
||||
<span>
|
||||
<span>{WORDMARK}</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type { PaneShellContextValue, PaneSlot } from './context'
|
||||
export { PaneShellContext } from './context'
|
||||
export { Pane, PaneMain, PaneShell } from './pane-shell'
|
||||
export { Pane, PANE_TOGGLE_REVEAL_EVENT, PaneMain, PaneShell } from './pane-shell'
|
||||
export type { PaneMainProps, PaneProps, PaneShellProps } from './pane-shell'
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -31,6 +32,12 @@ export interface PaneProps {
|
||||
defaultOpen?: boolean
|
||||
/** Forces the pane closed (track→0, aria-hidden) without writing to the store — for transient route gates. */
|
||||
disabled?: boolean
|
||||
/** Like disabled, but keeps hoverReveal alive — collapses the track without writing to the store (e.g. narrow window). */
|
||||
forceCollapsed?: boolean
|
||||
/** When collapsed, float the contents over the main column on hover/focus instead of hiding them (track stays 0px). */
|
||||
hoverReveal?: boolean
|
||||
/** Called with true while the pane is a collapsed hover-reveal overlay, so the consumer can keep contents mounted (ready to slide). */
|
||||
onOverlayActiveChange?: (overlayActive: boolean) => void
|
||||
id: string
|
||||
maxWidth?: WidthValue
|
||||
minWidth?: WidthValue
|
||||
@@ -53,6 +60,7 @@ export interface PaneShellProps {
|
||||
interface CollectedPane {
|
||||
defaultOpen: boolean
|
||||
disabled: boolean
|
||||
forceCollapsed: boolean
|
||||
id: string
|
||||
resizable: boolean
|
||||
side: PaneSide
|
||||
@@ -62,6 +70,22 @@ interface CollectedPane {
|
||||
const DEFAULT_WIDTH = '16rem'
|
||||
const DEFAULT_RESIZE_MIN_WIDTH = 160
|
||||
|
||||
// Hover-reveal slide. The enter delay is a pure-CSS hover-intent gate: a fast
|
||||
// pass-by doesn't dwell on the trigger long enough for the delay to elapse.
|
||||
const HOVER_REVEAL_SLIDE_MS = 220
|
||||
const HOVER_REVEAL_ENTER_DELAY_MS = 130
|
||||
const HOVER_REVEAL_EASE = 'cubic-bezier(0.32,0.72,0,1)'
|
||||
// Offset shadow lifting the revealed panel off the content (same both sides;
|
||||
// the mirror axis is offset-x, which is 0). Same color on light + dark.
|
||||
const HOVER_REVEAL_SHADOW = '0px -18px 18px -5px #00000012'
|
||||
// Edge trigger strip, inset past the OS window-resize grab area.
|
||||
const HOVER_REVEAL_TRIGGER_WIDTH = 14
|
||||
const HOVER_REVEAL_EDGE_GUTTER = 6
|
||||
|
||||
// Fired (window CustomEvent<{ id }>) to toggle a force-collapsed pane's reveal
|
||||
// from the keyboard, since its store-open toggle is a no-op while collapsed.
|
||||
export const PANE_TOGGLE_REVEAL_EVENT = 'hermes:pane-toggle-reveal'
|
||||
|
||||
const widthToCss = (value: WidthValue | undefined, fallback: string) =>
|
||||
value === undefined ? fallback : typeof value === 'number' ? `${value}px` : value
|
||||
|
||||
@@ -110,6 +134,7 @@ function collectPanes(children: ReactNode) {
|
||||
const entry: CollectedPane = {
|
||||
defaultOpen: props.defaultOpen ?? true,
|
||||
disabled: props.disabled ?? false,
|
||||
forceCollapsed: props.forceCollapsed ?? false,
|
||||
id: props.id,
|
||||
resizable: props.resizable ?? false,
|
||||
side: props.side,
|
||||
@@ -124,7 +149,7 @@ function collectPanes(children: ReactNode) {
|
||||
|
||||
function trackForPane(pane: CollectedPane, states: Record<string, { open: boolean; widthOverride?: number }>) {
|
||||
const stateOpen = states[pane.id]?.open ?? pane.defaultOpen
|
||||
const open = !pane.disabled && stateOpen
|
||||
const open = !pane.disabled && !pane.forceCollapsed && stateOpen
|
||||
|
||||
if (!open) {
|
||||
return { open: false, track: '0px' }
|
||||
@@ -193,14 +218,29 @@ export function Pane({
|
||||
className,
|
||||
defaultOpen = true,
|
||||
disabled = false,
|
||||
hoverReveal = false,
|
||||
id,
|
||||
maxWidth,
|
||||
minWidth,
|
||||
resizable = false
|
||||
onOverlayActiveChange,
|
||||
resizable = false,
|
||||
width
|
||||
}: PaneProps) {
|
||||
const ctx = useContext(PaneShellContext)
|
||||
const paneStates = useStore($paneStates)
|
||||
const registered = useRef(false)
|
||||
const paneRef = useRef<HTMLDivElement | null>(null)
|
||||
// Keyboard (mod+b / mod+j) pins the reveal open while collapsed; hover is CSS.
|
||||
const [forced, setForced] = useState(false)
|
||||
|
||||
const slot = ctx?.paneById.get(id)
|
||||
const open = Boolean(slot?.open && !disabled)
|
||||
const side = slot?.side ?? 'left'
|
||||
// Collapsed + hoverReveal: float the pane contents over the main column on
|
||||
// hover/focus instead of hiding them. Honors any persisted resize width.
|
||||
const overlayActive = !open && hoverReveal && !disabled
|
||||
const override = resizable ? paneStates[id]?.widthOverride : undefined
|
||||
const overlayWidth = override !== undefined ? `${override}px` : widthToCss(width, DEFAULT_WIDTH)
|
||||
|
||||
useEffect(() => {
|
||||
if (registered.current) {
|
||||
@@ -211,12 +251,34 @@ export function Pane({
|
||||
ensurePaneRegistered(id, { open: defaultOpen })
|
||||
}, [defaultOpen, id])
|
||||
|
||||
const slot = ctx?.paneById.get(id)
|
||||
const open = Boolean(slot?.open && !disabled)
|
||||
// Keyboard toggle pins/unpins the reveal while collapsed; clear when no longer
|
||||
// a collapsed overlay (reopened / widened).
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !overlayActive) {
|
||||
setForced(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const onToggle = (e: Event) => {
|
||||
if ((e as CustomEvent<{ id: string }>).detail?.id === id) {
|
||||
setForced(v => !v)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener(PANE_TOGGLE_REVEAL_EVENT, onToggle)
|
||||
|
||||
return () => window.removeEventListener(PANE_TOGGLE_REVEAL_EVENT, onToggle)
|
||||
}, [id, overlayActive])
|
||||
|
||||
// Keep contents mounted while collapsed so reveal is a pure CSS transform.
|
||||
useEffect(() => {
|
||||
onOverlayActiveChange?.(overlayActive)
|
||||
}, [onOverlayActiveChange, overlayActive])
|
||||
|
||||
const canResize = open && resizable
|
||||
const lo = widthToPx(minWidth) ?? DEFAULT_RESIZE_MIN_WIDTH
|
||||
const hi = widthToPx(maxWidth) ?? Number.POSITIVE_INFINITY
|
||||
const side = slot?.side ?? 'left'
|
||||
|
||||
const startResize = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
@@ -273,6 +335,58 @@ export function Pane({
|
||||
return null
|
||||
}
|
||||
|
||||
// Collapsed hover-reveal track: a 0px, pointer-transparent grid cell holding a
|
||||
// thin edge trigger + the floating panel (both absolute, escaping the zero
|
||||
// box). group-hover (or data-forced from the keyboard) drives the slide; the
|
||||
// enter-delay is the hover-intent gate. No JS pointer math.
|
||||
if (overlayActive) {
|
||||
const edge = side === 'left' ? 'left' : 'right'
|
||||
const offscreen = side === 'left' ? '-translate-x-[calc(100%+1rem)]' : 'translate-x-[calc(100%+1rem)]'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group/reveal pointer-events-none relative row-start-1 min-w-0', className)}
|
||||
data-forced={forced ? '' : undefined}
|
||||
data-pane-hover-reveal={forced ? 'open' : 'closed'}
|
||||
data-pane-id={id}
|
||||
data-pane-open="false"
|
||||
data-pane-side={side}
|
||||
ref={paneRef}
|
||||
style={{ gridColumn: `${slot.column} / ${slot.column + 1}` }}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-auto absolute inset-y-0 z-30 [-webkit-app-region:no-drag]"
|
||||
style={{ [edge]: HOVER_REVEAL_EDGE_GUTTER, width: HOVER_REVEAL_TRIGGER_WIDTH }}
|
||||
/>
|
||||
|
||||
{/* Keyed on side so flipping panes remounts off-screen on the new edge
|
||||
instead of transitioning the transform across the viewport. */}
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-y-0 z-30 overflow-hidden transition-transform delay-0',
|
||||
offscreen,
|
||||
'group-hover/reveal:pointer-events-auto group-hover/reveal:translate-x-0 group-hover/reveal:delay-[var(--reveal-enter-delay)] group-hover/reveal:shadow-[var(--reveal-shadow)]',
|
||||
'group-data-[forced]/reveal:pointer-events-auto group-data-[forced]/reveal:translate-x-0 group-data-[forced]/reveal:delay-0 group-data-[forced]/reveal:shadow-[var(--reveal-shadow)]'
|
||||
)}
|
||||
key={edge}
|
||||
style={
|
||||
{
|
||||
[edge]: 0,
|
||||
width: overlayWidth,
|
||||
'--reveal-shadow': HOVER_REVEAL_SHADOW,
|
||||
transitionDuration: `${HOVER_REVEAL_SLIDE_MS}ms`,
|
||||
transitionTimingFunction: HOVER_REVEAL_EASE,
|
||||
'--reveal-enter-delay': `${HOVER_REVEAL_ENTER_DELAY_MS}ms`
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden={!open}
|
||||
|
||||
7
apps/desktop/src/global.d.ts
vendored
@@ -7,6 +7,13 @@ declare global {
|
||||
// the window's backend; pass a named profile to lazily spawn/reuse that
|
||||
// profile's backend from the pool.
|
||||
getConnection: (profile?: string | null) => Promise<HermesConnection>
|
||||
// Reconnect-after-wake recovery: liveness-probe the cached PRIMARY backend
|
||||
// and drop it if a remote one has gone unreachable, so the next
|
||||
// getConnection() rebuilds a reachable descriptor instead of the renderer
|
||||
// re-dialing a dead remote forever. No-op for local backends (they
|
||||
// self-heal via the child 'exit' handler). `rebuilt` is true when a stale
|
||||
// remote cache was dropped.
|
||||
revalidateConnection: () => Promise<{ ok: boolean; rebuilt: boolean }>
|
||||
// Keepalive: mark a pool profile backend as recently used so the idle
|
||||
// reaper spares it while its chat is active.
|
||||
touchBackend: (profile?: string | null) => Promise<{ ok: boolean }>
|
||||
|
||||
@@ -1463,8 +1463,8 @@ export const en: Translations = {
|
||||
contextUsage: 'Context usage',
|
||||
session: 'Session',
|
||||
runtimeSessionElapsed: 'Runtime session elapsed',
|
||||
yoloOn: 'YOLO on — auto-approving dangerous commands. Click to turn off.',
|
||||
yoloOff: 'YOLO off — click to auto-approve dangerous commands.',
|
||||
yoloOn: 'YOLO on — auto-approving dangerous commands. Click to turn off. Shift+click toggles it globally.',
|
||||
yoloOff: 'YOLO off — click to auto-approve dangerous commands. Shift+click toggles it globally.',
|
||||
modelNone: 'none',
|
||||
noModel: 'no model',
|
||||
switchModel: 'Switch model',
|
||||
|
||||
@@ -1606,8 +1606,8 @@ export const ja = defineLocale({
|
||||
contextUsage: 'コンテキスト使用状況',
|
||||
session: 'セッション',
|
||||
runtimeSessionElapsed: 'ランタイムセッション経過時間',
|
||||
yoloOn: 'YOLO オン — 危険なコマンドを自動承認中。クリックでオフに。',
|
||||
yoloOff: 'YOLO オフ — クリックで危険なコマンドを自動承認。',
|
||||
yoloOn: 'YOLO オン — 危険なコマンドを自動承認中。クリックでオフに。Shift+クリックで全体に切り替え。',
|
||||
yoloOff: 'YOLO オフ — クリックで危険なコマンドを自動承認。Shift+クリックで全体に切り替え。',
|
||||
modelNone: 'なし',
|
||||
noModel: 'モデルなし',
|
||||
switchModel: 'モデルを切り替え',
|
||||
|
||||
@@ -1567,8 +1567,8 @@ export const zhHant = defineLocale({
|
||||
contextUsage: '上下文使用量',
|
||||
session: '工作階段',
|
||||
runtimeSessionElapsed: '執行時工作階段已用時間',
|
||||
yoloOn: 'YOLO 已開啟 — 自動核准危險指令。點擊關閉。',
|
||||
yoloOff: 'YOLO 已關閉 — 點擊自動核准危險指令。',
|
||||
yoloOn: 'YOLO 已開啟 — 自動核准危險指令。點擊關閉。Shift+點擊可全域切換。',
|
||||
yoloOff: 'YOLO 已關閉 — 點擊自動核准危險指令。Shift+點擊可全域切換。',
|
||||
modelNone: '無',
|
||||
noModel: '無模型',
|
||||
switchModel: '切換模型',
|
||||
|
||||
@@ -1644,8 +1644,8 @@ export const zh: Translations = {
|
||||
contextUsage: '上下文用量',
|
||||
session: '会话',
|
||||
runtimeSessionElapsed: '运行时会话已用时间',
|
||||
yoloOn: 'YOLO 已开启 - 自动批准危险命令。点击关闭。',
|
||||
yoloOff: 'YOLO 已关闭 - 点击自动批准危险命令。',
|
||||
yoloOn: 'YOLO 已开启 - 自动批准危险命令。点击关闭。Shift+点击可全局切换。',
|
||||
yoloOff: 'YOLO 已关闭 - 点击自动批准危险命令。Shift+点击可全局切换。',
|
||||
modelNone: '无',
|
||||
noModel: '无模型',
|
||||
switchModel: '切换模型',
|
||||
|
||||
@@ -52,6 +52,14 @@ describe('sessionMatchesSearch', () => {
|
||||
expect(sessionMatchesSearch(session, 'hermes-agent')).toBe(true)
|
||||
})
|
||||
|
||||
it('matches sessions by source platform and aliases', () => {
|
||||
expect(sessionMatchesSearch(makeSession({ source: 'telegram' }), 'Telegram')).toBe(true)
|
||||
expect(sessionMatchesSearch(makeSession({ source: 'whatsapp' }), 'WhatsApp')).toBe(true)
|
||||
expect(sessionMatchesSearch(makeSession({ source: 'whatsapp' }), 'wa')).toBe(true)
|
||||
expect(sessionMatchesSearch(makeSession({ source: 'slack' }), 'slack')).toBe(true)
|
||||
expect(sessionMatchesSearch(makeSession({ source: 'bluebubbles' }), 'imessage')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not match unrelated queries', () => {
|
||||
expect(sessionMatchesSearch(makeSession(), 'totally-unrelated')).toBe(false)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SessionInfo } from '@/types/hermes'
|
||||
|
||||
import { sessionTitle } from './chat-runtime'
|
||||
import { sessionSourceSearchTerms } from './session-source'
|
||||
|
||||
export function sessionMatchesSearch(session: SessionInfo, query: string): boolean {
|
||||
const needle = query.trim().toLowerCase()
|
||||
@@ -14,6 +15,7 @@ export function sessionMatchesSearch(session: SessionInfo, query: string): boole
|
||||
session._lineage_root_id ?? '',
|
||||
sessionTitle(session),
|
||||
session.preview ?? '',
|
||||
session.cwd ?? ''
|
||||
session.cwd ?? '',
|
||||
...sessionSourceSearchTerms(session.source)
|
||||
].some(value => value.toLowerCase().includes(needle))
|
||||
}
|
||||
|
||||
62
apps/desktop/src/lib/session-source.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
api_server: 'API',
|
||||
bluebubbles: 'iMessage',
|
||||
cli: 'CLI',
|
||||
codex: 'Codex',
|
||||
desktop: 'Desktop',
|
||||
discord: 'Discord',
|
||||
email: 'Email',
|
||||
gateway: 'Gateway',
|
||||
local: 'Local',
|
||||
matrix: 'Matrix',
|
||||
mattermost: 'Mattermost',
|
||||
qqbot: 'QQ',
|
||||
signal: 'Signal',
|
||||
slack: 'Slack',
|
||||
sms: 'SMS',
|
||||
telegram: 'Telegram',
|
||||
tui: 'TUI',
|
||||
webhook: 'Webhook',
|
||||
weixin: 'WeChat',
|
||||
whatsapp: 'WhatsApp',
|
||||
yuanbao: 'Yuanbao'
|
||||
}
|
||||
|
||||
const SOURCE_ALIASES: Record<string, string[]> = {
|
||||
bluebubbles: ['apple messages', 'imessage'],
|
||||
cli: ['terminal'],
|
||||
desktop: ['app', 'gui'],
|
||||
local: ['machine'],
|
||||
qqbot: ['qq'],
|
||||
telegram: ['tg'],
|
||||
tui: ['terminal'],
|
||||
weixin: ['wechat'],
|
||||
whatsapp: ['wa']
|
||||
}
|
||||
|
||||
export function normalizeSessionSource(source: null | string | undefined): string | null {
|
||||
const id = source?.trim().toLowerCase()
|
||||
|
||||
return id || null
|
||||
}
|
||||
|
||||
export function sessionSourceLabel(source: null | string | undefined): string | null {
|
||||
const id = normalizeSessionSource(source)
|
||||
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
|
||||
return SOURCE_LABELS[id] || id.replace(/[_-]+/g, ' ').replace(/\b\w/g, char => char.toUpperCase())
|
||||
}
|
||||
|
||||
export function sessionSourceSearchTerms(source: null | string | undefined): string[] {
|
||||
const id = normalizeSessionSource(source)
|
||||
const label = sessionSourceLabel(id)
|
||||
|
||||
if (!id) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [id, label ?? '', ...(SOURCE_ALIASES[id] ?? [])].filter(Boolean)
|
||||
}
|
||||
@@ -24,3 +24,27 @@ export async function setSessionYolo(
|
||||
|
||||
return active
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle GLOBAL YOLO (approval bypass) via gateway `config.set` with
|
||||
* `scope: 'global'`. This flips the persistent `approvals.mode` in config.yaml
|
||||
* between `off` (bypass on) and `manual` (bypass off), affecting every session,
|
||||
* the CLI, the TUI, and cron — and it survives restarts. Triggered by
|
||||
* Shift+clicking the status-bar zap.
|
||||
*/
|
||||
export async function setGlobalYolo(
|
||||
requestGateway: GatewayRequester,
|
||||
enabled: boolean
|
||||
): Promise<boolean> {
|
||||
const result = await requestGateway<{ value?: string }>('config.set', {
|
||||
key: 'yolo',
|
||||
scope: 'global',
|
||||
value: enabled ? '1' : '0'
|
||||
})
|
||||
|
||||
const active = result?.value === '1'
|
||||
|
||||
setYoloActive(active)
|
||||
|
||||
return active
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export const SIDEBAR_SESSIONS_PAGE_SIZE = 50
|
||||
const SIDEBAR_PINNED_STORAGE_KEY = 'hermes.desktop.pinnedSessions'
|
||||
const SIDEBAR_AGENTS_GROUPED_STORAGE_KEY = 'hermes.desktop.agentsGroupedByWorkspace'
|
||||
const SIDEBAR_CRON_OPEN_STORAGE_KEY = 'hermes.desktop.sidebarCronOpen'
|
||||
const SIDEBAR_SESSION_ORDER_STORAGE_KEY = 'hermes.desktop.sessionOrder'
|
||||
const SIDEBAR_WORKSPACE_ORDER_STORAGE_KEY = 'hermes.desktop.workspaceOrder'
|
||||
const PANES_FLIPPED_STORAGE_KEY = 'hermes.desktop.panesFlipped'
|
||||
|
||||
export const CHAT_SIDEBAR_PANE_ID = 'chat-sidebar'
|
||||
@@ -53,7 +55,14 @@ export const $sidebarWidth: ReadableAtom<number> = computed($paneStates, states
|
||||
})
|
||||
|
||||
export const $pinnedSessionIds = atom(storedStringArray(SIDEBAR_PINNED_STORAGE_KEY))
|
||||
export const $sidebarSessionOrderIds = atom(storedStringArray(SIDEBAR_SESSION_ORDER_STORAGE_KEY))
|
||||
export const $sidebarWorkspaceOrderIds = atom(storedStringArray(SIDEBAR_WORKSPACE_ORDER_STORAGE_KEY))
|
||||
export const $sidebarPinsOpen = atom(true)
|
||||
// Set by the PaneShell hover-reveal overlay while the sidebar is collapsed; kept
|
||||
// true the whole time it's a floating overlay (not just while shown) so the
|
||||
// consumer mounts contents off-screen, ready to slide. ChatSidebar mounts its
|
||||
// rows on `sidebarOpen || this`.
|
||||
export const $sidebarOverlayMounted = atom(false)
|
||||
export const $sidebarRecentsOpen = atom(true)
|
||||
// Cron-job sessions live in their own section below recents, collapsed by
|
||||
// default (it only renders at all when cron sessions exist) so the
|
||||
@@ -68,6 +77,8 @@ export const $sessionsLimit = atom(SIDEBAR_SESSIONS_PAGE_SIZE)
|
||||
|
||||
$pinnedSessionIds.subscribe(ids => persistStringArray(SIDEBAR_PINNED_STORAGE_KEY, [...ids]))
|
||||
$sidebarCronOpen.subscribe(open => persistBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, open))
|
||||
$sidebarSessionOrderIds.subscribe(ids => persistStringArray(SIDEBAR_SESSION_ORDER_STORAGE_KEY, [...ids]))
|
||||
$sidebarWorkspaceOrderIds.subscribe(ids => persistStringArray(SIDEBAR_WORKSPACE_ORDER_STORAGE_KEY, [...ids]))
|
||||
$sidebarAgentsGrouped.subscribe(grouped => persistBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, grouped))
|
||||
$panesFlipped.subscribe(flipped => persistBoolean(PANES_FLIPPED_STORAGE_KEY, flipped))
|
||||
|
||||
@@ -116,6 +127,10 @@ export function setSidebarPinsOpen(open: boolean) {
|
||||
$sidebarPinsOpen.set(open)
|
||||
}
|
||||
|
||||
export function setSidebarOverlayMounted(mounted: boolean) {
|
||||
$sidebarOverlayMounted.set(mounted)
|
||||
}
|
||||
|
||||
export function setSidebarRecentsOpen(open: boolean) {
|
||||
$sidebarRecentsOpen.set(open)
|
||||
}
|
||||
@@ -128,6 +143,18 @@ export function setSidebarAgentsGrouped(grouped: boolean) {
|
||||
$sidebarAgentsGrouped.set(grouped)
|
||||
}
|
||||
|
||||
export function setSidebarSessionOrderIds(ids: string[]) {
|
||||
if (!arraysEqual($sidebarSessionOrderIds.get(), ids)) {
|
||||
$sidebarSessionOrderIds.set(ids)
|
||||
}
|
||||
}
|
||||
|
||||
export function setSidebarWorkspaceOrderIds(ids: string[]) {
|
||||
if (!arraysEqual($sidebarWorkspaceOrderIds.get(), ids)) {
|
||||
$sidebarWorkspaceOrderIds.set(ids)
|
||||
}
|
||||
}
|
||||
|
||||
export function setSidebarResizing(resizing: boolean) {
|
||||
$isSidebarResizing.set(resizing)
|
||||
}
|
||||
|
||||
@@ -888,52 +888,42 @@ canvas {
|
||||
}
|
||||
|
||||
.fit-text {
|
||||
--fit-captured-length: initial;
|
||||
--fit-support-sentinel: var(--fit-captured-length, 9999px);
|
||||
|
||||
display: flex;
|
||||
font-size: var(--fit-text-min, 1rem);
|
||||
container-type: inline-size;
|
||||
--captured-length: initial;
|
||||
--support-sentinel: var(--captured-length, 9999px);
|
||||
}
|
||||
|
||||
.fit-text > [aria-hidden='true'] {
|
||||
.fit-text > [aria-hidden] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.fit-text > :not([aria-hidden='true']) {
|
||||
.fit-text > :not([aria-hidden]) {
|
||||
flex-grow: 1;
|
||||
container-type: inline-size;
|
||||
--captured-length: 100cqi;
|
||||
--available-space: var(--captured-length);
|
||||
|
||||
--fit-captured-length: 100cqi;
|
||||
--fit-available-space: var(--fit-captured-length);
|
||||
}
|
||||
|
||||
.fit-text > :not([aria-hidden='true']) > * {
|
||||
.fit-text > :not([aria-hidden]) > * {
|
||||
--fit-support-sentinel: inherit;
|
||||
--fit-captured-length: 100cqi;
|
||||
--fit-ratio: tan(atan2(var(--fit-available-space), var(--fit-available-space) - var(--fit-captured-length)));
|
||||
|
||||
display: block;
|
||||
inline-size: var(--available-space);
|
||||
line-height: var(--fit-text-line-height, 1);
|
||||
--support-sentinel: inherit;
|
||||
--captured-length: 100cqi;
|
||||
--ratio: tan(atan2(var(--available-space), var(--available-space) - var(--captured-length)));
|
||||
--font-size: clamp(
|
||||
var(--fit-text-min, 1em),
|
||||
1em * var(--ratio),
|
||||
var(--fit-text-max, infinity * 1px) - var(--support-sentinel)
|
||||
);
|
||||
font-size: var(--font-size);
|
||||
inline-size: var(--fit-available-space);
|
||||
font-size: clamp(var(--fit-min, 1em), 1em * var(--fit-ratio), var(--fit-max, infinity * 1px) - var(--fit-support-sentinel));
|
||||
}
|
||||
|
||||
@container (inline-size > 0) {
|
||||
.fit-text > :not([aria-hidden='true']) > * {
|
||||
.fit-text > :not([aria-hidden]) > * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@property --captured-length {
|
||||
syntax: '<length>';
|
||||
initial-value: 0px;
|
||||
inherits: true;
|
||||
}
|
||||
|
||||
@property --captured-length2 {
|
||||
@property --fit-captured-length {
|
||||
syntax: '<length>';
|
||||
initial-value: 0px;
|
||||
inherits: true;
|
||||
|
||||
@@ -6,6 +6,19 @@ import path from 'path'
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [react(), tailwindcss()],
|
||||
css: {
|
||||
// Pin an explicit (empty) PostCSS config. Tailwind is handled entirely by
|
||||
// `@tailwindcss/vite`, so the renderer needs no PostCSS plugins — and
|
||||
// without this, Vite's `postcss-load-config` walks UP the filesystem
|
||||
// looking for a stray `postcss.config.*` / `tailwind.config.*`. The desktop
|
||||
// build runs from inside the user's home tree (e.g.
|
||||
// `C:\Users\<name>\AppData\Local\hermes\hermes-agent\apps\desktop`), so an
|
||||
// unrelated Tailwind v3 config higher up the tree gets picked up and
|
||||
// reprocesses our v4 stylesheet, failing the build with
|
||||
// "`@layer base` is used but no matching `@tailwind base` directive is
|
||||
// present." Pinning the config makes the build hermetic.
|
||||
postcss: { plugins: [] }
|
||||
},
|
||||
build: {
|
||||
// Keep desktop packaging stable: Shiki ships many dynamic chunks by
|
||||
// default, and electron-builder can OOM scanning thousands of files.
|
||||
|
||||
3
bench/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.cache/
|
||||
*.cpuprofile
|
||||
148
bench/README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# TUI benchmark suite — Ink (`ui-tui`) vs OpenTUI (`ui-opentui`)
|
||||
|
||||
Methodology (settled, binding): `docs/plans/opentui-bench-suite.md`. This
|
||||
directory is the implementation: real binaries over a real node-pty PTY
|
||||
(120×40, xterm-256color), a fake gateway substituted via `HERMES_PYTHON`
|
||||
(ZERO changes to either UI), external `/proc` sampling, cgroup-v2 memory caps.
|
||||
No tmux anywhere in measurement — except the `pipeline` cell, whose entire
|
||||
point is measuring the tmux emulator leg (see its note below).
|
||||
|
||||
## Pieces
|
||||
|
||||
| file | role |
|
||||
|---|---|
|
||||
| `fake-gateway.mjs` | NDJSON JSON-RPC gateway stand-in. Both UIs spawn it as `$HERMES_PYTHON -m tui_gateway.entry`. Answers every startup RPC with canned results, then streams the fixture (burst / paced / load-then-idle). Never writes stderr (the UIs render gateway stderr). |
|
||||
| `fixture-stream.mjs` | Serializes the deterministic lumpy-turn fixture (`ui-opentui/scripts/fixture.ts`, imported directly via Node ≥26 type stripping — no port) to NDJSON. Cached under `.cache/`, sha256-stamped. |
|
||||
| `harness.mjs` | One scenario = one UI boot: node-pty PTY, tight drain loop (event-loop starvation probe, 10ms budget asserted), `/proc/PID/{smaps_rollup,status,stat}` samples on 100-msg boundaries (UI PID only), `systemd-run --user --scope -p MemoryMax=… -p MemorySwapMax=0` caps, SGR wheel injection, resize-jiggle digest capture. |
|
||||
| `run.mjs` | The matrix runner (protocol: determinism gate first, sequential SUTs, randomized per-rep config order, 10s cooldowns, load gate). |
|
||||
| `render.mjs` | `results/*.json` → self-contained `report.html` (inline SVG, no CDN) + PNGs in `report-assets/`. |
|
||||
|
||||
## Running cells
|
||||
|
||||
Node 26 is required (`BENCH_NODE_BIN` overrides the default fnm path). Build
|
||||
both UIs first; results land in `results/<utc>-<sha7>-<cell>-<ui>-<config>-r<rep>.json`.
|
||||
|
||||
```sh
|
||||
cd ui-opentui && node scripts/build.mjs && cd ../ui-tui && node scripts/build.mjs && cd ../bench
|
||||
npm install # node-pty (bench-local devDep)
|
||||
|
||||
node run.mjs --cell gate # determinism gate (digest replay ×2 per UI) — run FIRST
|
||||
node run.mjs --cell mem3000 # clean memory runs, 3 reps × 3 configs, 2GB cap
|
||||
node run.mjs --cell slope10k # one 10k-msg slope run: ink + otui-uncapped (cap-hit IS a datapoint)
|
||||
node run.mjs --cell nodes # instrumented node counts (ink fd-3 sampler; opentui headless walk)
|
||||
node run.mjs --cell cpu # paced 30 ev/s streaming ×3
|
||||
node run.mjs --cell scroll # SGR wheel 30Hz×15s on a 3000-msg transcript ×3
|
||||
node run.mjs --cell startup # ×10, fake gateway
|
||||
node run.mjs --cell chaos # stability: gw SIGKILL mid-stream/mid-tool, SIGSTOP 30s, resize storm, PTY EOF — 5 scenarios × {ink, otui-capped}
|
||||
node run.mjs --cell pipeline # total-pipeline CPU: UI inside a DEDICATED tmux server (the user's real emulator leg), /proc utime+stime for UI + gateway + tmux @1Hz
|
||||
node run.mjs --cell echo # M7 input latency: 30 keystrokes → first echoed paint (p50/p95/p99) + one \r submit → first-token-paint
|
||||
node render.mjs # report.html + report-assets/*.png
|
||||
```
|
||||
|
||||
### Chaos / pipeline / echo cell notes
|
||||
|
||||
- **chaos** (5 scenarios × ink/otui-capped, one JSON each, `summary.chaos`):
|
||||
gateway death is SELF-inflicted (`HERMES_FAKE_DIE_AT=<msg>:<kill|tool-kill>`
|
||||
→ SIGKILL at fixture msg N, or at the first `tool.*` event after N) because
|
||||
self-termination is deterministic vs racy external timing; a die-once flag
|
||||
file keeps the auto-heal respawn from dying again. SIGSTOP (gw-stop) is
|
||||
external via `HERMES_FAKE_PIDFILE`. Respawn detection = the respawned
|
||||
gateway REWRITING that pidfile. Both UIs auto-heal (budget 3 respawns/60s):
|
||||
OpenTUI with exponential backoff (`ui-opentui/src/boundary/gateway/liveGateway.ts`
|
||||
`onExit`), Ink immediately (`ui-tui/src/app/useMainApp.ts` `exitHandler`).
|
||||
`transcript_preserved` = after a forced full repaint (resize jiggle), the
|
||||
screen still shows a recent pre-kill turn (`const xN` fixture markers).
|
||||
`summary.result` keeps its usual semantics — for pty-eof the UI *should*
|
||||
die, so read `summary.chaos`, not `summary.result`, for the verdicts.
|
||||
- **pipeline**: the ONLY cell that uses tmux — deliberately. The user's real
|
||||
stack runs the TUI inside tmux (verified via /proc environ), so a dedicated
|
||||
`tmux -L hermes-bench-<runId> -f /dev/null` server is the locally measurable
|
||||
terminal-emulator leg. The harness PTY attaches a client (unattached tmux
|
||||
skips most output work; `data_flowing` asserts bytes actually arrived) and
|
||||
samples /proc utime+stime at 1Hz for UI, fake gateway, and the tmux server
|
||||
(`summary.pipeline.cpu_s`). Only that socket's server is killed at the end.
|
||||
Note tmux re-encodes the UI's output for the outer client, so `pty_bytes_total`
|
||||
here is the post-tmux byte count, not the UI's raw output.
|
||||
- **frame pacing (M6)**: cpu-paced and pipeline record every PTY chunk
|
||||
timestamp+size; bursts separated by >4ms gaps are frames →
|
||||
`summary.frame_pacing` (fps, interframe p50/p95, bytes/frame p50/p95,
|
||||
coalesced count). Scroll runs record the wheel phase only. There is no
|
||||
env-gated renderer frame counter in ui-opentui to use as ground truth —
|
||||
@opentui/core keeps `renderStats.fps` internally but nothing exports it;
|
||||
wiring it would need a ui-source patch (out of scope here).
|
||||
- **echo (M7)**: keystroke chars avoid `u`/`p`/`s`/digits (the OpenTUI status
|
||||
clock repaints `up: Ns` at 1Hz) and matching runs on ANSI-stripped output
|
||||
(raw chunks are full of CSI final letters). The submit leg works because the
|
||||
fake gateway answers `prompt.submit` with a tiny streamed reply carrying the
|
||||
marker token `zqxjv` when `HERMES_FAKE_SUBMIT_RESPONSE=1`.
|
||||
|
||||
Configs: `ink` · `otui-capped` (`HERMES_TUI_MAX_MESSAGES=3000`, the default) ·
|
||||
`otui-uncapped` (`=100000`). Launch parity with `hermes_cli/main.py`:
|
||||
Ink = `node --expose-gc ui-tui/dist/entry.js`, OpenTUI =
|
||||
`node --experimental-ffi --no-warnings ui-opentui/dist/main.js`, both with
|
||||
`NODE_OPTIONS=--max-old-space-size=<heap>` (8192 on the unconstrained host —
|
||||
what the launcher picks outside a container).
|
||||
|
||||
## E3 (constrained Docker survival)
|
||||
|
||||
`E3-lite` runs the same harness inside a generic `node:26` container (NOT the
|
||||
shipped image) with the worktree bind-mounted read-only and `--memory=1g
|
||||
--memory-swap=1g`; the whole container (UI + fake gateway + harness) shares the
|
||||
limit. See `run-e3.sh` if present, or the report's survival table for the exact
|
||||
invocation used.
|
||||
|
||||
## What actually ran on 2026-06-11 (E1 host + E3-lite) — deviations from the plan
|
||||
|
||||
- **3 reps** for mem3000 (not 5) and **scroll at 2000 msgs** (not 3000): the
|
||||
OpenTUI engine on this tree (sha 197d499, dist built from 50e3471 tree state)
|
||||
**crashes at ≈3000 fixture msgs** — an uncaught `Error: Failed to create
|
||||
SyntaxStyle` (native handle allocation fails; every `TextBufferRenderable`
|
||||
creates one in @opentui/core 0.4.0), masked by a second
|
||||
`Failed to create optimized buffer` crash inside the renderer's
|
||||
uncaughtException handler. Postmortems are in each result's `pty_tail`;
|
||||
RSS at crash ≈880MB — far below the 2GB cap, so it is a handle/pool limit,
|
||||
not memory. This dominates every OpenTUI cell past ~3000 msgs.
|
||||
- **OpenTUI headless node-count: not run.** `scripts/mem-bench.tsx` under Node
|
||||
FFI dies on the first fixture turn with `ERR_INVALID_ARG_VALUE …
|
||||
textBufferViewSetViewport` (the known Bun→Node u32-coordinate class; the
|
||||
production binary carries the ffiSafe clamp, the headless test renderer path
|
||||
does not) and then hangs. The Ink fd-3 sampler ran fine.
|
||||
- **Startup real-gateway variant: probed, not run as a cell.** A full run would
|
||||
forge real sessions in the user's `~/.hermes` store. Measured standalone:
|
||||
the real `tui_gateway` (venv python) emits `gateway.ready` in **131ms median**
|
||||
(×10, range 130–138ms) — add that to the fake-gateway startup numbers.
|
||||
- **No cgroup OOM kills observed** anywhere (Ink at 10k msgs peaks ~321MB;
|
||||
OpenTUI crashes before reaching the cap), so the cap-hit machinery
|
||||
(memory.events / journal fallback) never fired in anger; E3-lite classified
|
||||
the OpenTUI death correctly as a crash (`oom_kill=0`, exit 7).
|
||||
- E2 (shipped Docker image): not run — image build time prohibitive in this
|
||||
session; E3-lite (generic node:26) covers the constrained-memory question.
|
||||
- Drain-loop starvation: a handful of OpenTUI burst runs recorded 11–18ms max
|
||||
event-loop lag in the harness (>10ms budget, flagged `drain_ok:false` in
|
||||
those results); all paced/scroll/startup runs stayed under 10ms.
|
||||
|
||||
## Accounting + known deviations (by design)
|
||||
|
||||
- **"messages" = fixture rows** (`rowsPerTurn` accounting, identical to
|
||||
`ui-opentui/scripts/mem-bench.tsx`), so numbers are comparable with the
|
||||
pre-registered expectations. ~46% of fixture rows are user/system rows.
|
||||
- **User/system rows are not streamed**: they are composer-local in both UIs
|
||||
(no wire event exists), so PTY runs mount only the assistant/tool rows —
|
||||
the renderable-heavy part that carries the memory claim. Consequence: the
|
||||
OpenTUI store cap (3000 rows) binds at ≈6.6k fixture-msgs in PTY runs.
|
||||
- **Digest gate**: final-screen digest after a resize-forced repaint, ANSI
|
||||
stripped, cut at the composer hint, `up: Ns` normalized (the OpenTUI status
|
||||
bar has a 1Hz uptime clock; the transcript region itself is deterministic).
|
||||
- The headless `scripts/mem-bench.tsx` numbers are diagnostic-only and flagged
|
||||
`instrumented`/`diagnostic_only` — never headlined.
|
||||
|
||||
## Build/run parity vs an installed hermes (audit, 2026-06-11)
|
||||
- Both UIs are built by their own repo build scripts (same artifacts an install produces) and
|
||||
spawned at their real entries: otui `node --experimental-ffi --no-warnings dist/main.js`
|
||||
(identical to production); ink `dist/entry.js` with env mirroring `_launch_tui`
|
||||
(NODE_ENV=production).
|
||||
- Two deviations: (1) ink's spawn adds `--expose-gc` — audited: nothing ever calls gc(), the
|
||||
flag is inert; kept for the instrumented sampler runs, harmless in clean runs. (2) both UIs
|
||||
run on the pinned Node 26.3 per protocol ("never compare across Node majors") — installed ink
|
||||
commonly runs Node 20/22, so ink's ABSOLUTE numbers are "ink on Node 26"; the relative
|
||||
comparison is unaffected. An as-installed-Node ink re-run is a worthwhile extra cell.
|
||||
239
bench/fake-gateway.mjs
Executable file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env node
|
||||
// Fake tui_gateway — substituted via HERMES_PYTHON so BOTH UIs spawn THIS
|
||||
// executable as `$HERMES_PYTHON -m tui_gateway.entry` (argv ignored) and speak
|
||||
// the identical NDJSON JSON-RPC wire over stdio. ZERO changes to either UI.
|
||||
//
|
||||
// Wire contract (mirrors tui_gateway/entry.py + both UI clients):
|
||||
// - unsolicited {jsonrpc:"2.0",method:"event",params:{type:"gateway.ready",payload:{skin:{}}}}
|
||||
// - events: {jsonrpc:"2.0",method:"event",params:{type,payload?}} (no id)
|
||||
// - responses: {jsonrpc:"2.0",id,result} for every request, canned per method.
|
||||
//
|
||||
// NEVER writes to stderr (both UIs surface gateway stderr lines INTO the UI as
|
||||
// activity rows / gateway.stderr events, which would perturb the rendered
|
||||
// transcript). Progress/telemetry goes to HERMES_FAKE_PROGRESS (append-only
|
||||
// NDJSON file the harness tails).
|
||||
//
|
||||
// Env config:
|
||||
// HERMES_FAKE_FIXTURE NDJSON fixture path (from fixture-stream.mjs). Optional.
|
||||
// HERMES_FAKE_MODE burst | paced | load-then-idle (default burst)
|
||||
// HERMES_FAKE_RATE events/sec for paced mode (default 30)
|
||||
// HERMES_FAKE_START_DELAY_MS delay after session.create reply before streaming (default 1500)
|
||||
// HERMES_FAKE_SAMPLE_EVERY fixture-msg boundary cadence for progress lines (default 100)
|
||||
// HERMES_FAKE_PROGRESS progress NDJSON file path (required for harness runs)
|
||||
// HERMES_FAKE_PIDFILE write own pid here at startup (harness discovers the
|
||||
// gateway pid; a REWRITE by a respawned instance is the
|
||||
// harness's auto-heal detection signal)
|
||||
// HERMES_FAKE_DIE_AT "<msgIndex>:<kill|tool-kill>" — chaos cells: self-SIGKILL
|
||||
// at fixture msg N (kill), or at the first tool.* event
|
||||
// after msg N (tool-kill). Self-termination is deterministic
|
||||
// vs racy external timing. SIGSTOP stays external (a stopped
|
||||
// process can't stop itself usefully).
|
||||
// HERMES_FAKE_DIE_FLAG die-once flag file: created just before the self-kill so
|
||||
// the UI's auto-heal RESPAWN (same env) does not die again
|
||||
// HERMES_FAKE_SUBMIT_RESPONSE "1" → answer prompt.submit with a tiny streamed reply
|
||||
// carrying the marker token "zqxjv" (echo-latency cells)
|
||||
//
|
||||
// Modes: burst = write as fast as the pipe accepts (await 'drain' on
|
||||
// backpressure, so emission tracks UI ingestion within the ~64KB pipe buffer);
|
||||
// paced = HERMES_FAKE_RATE events/sec; load-then-idle = burst, then sit idle
|
||||
// (scroll-latency runs drive input afterwards). Exits on stdin EOF (the UIs
|
||||
// close stdin to stop the gateway) — same lifecycle as the real child.
|
||||
|
||||
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { createInterface } from 'node:readline'
|
||||
|
||||
const FIXTURE = process.env.HERMES_FAKE_FIXTURE || ''
|
||||
const MODE = process.env.HERMES_FAKE_MODE || 'burst'
|
||||
const RATE = Math.max(1, Number.parseInt(process.env.HERMES_FAKE_RATE ?? '30', 10) || 30)
|
||||
const START_DELAY_MS = Number.parseInt(process.env.HERMES_FAKE_START_DELAY_MS ?? '1500', 10) || 1500
|
||||
const SAMPLE_EVERY = Math.max(1, Number.parseInt(process.env.HERMES_FAKE_SAMPLE_EVERY ?? '100', 10) || 100)
|
||||
const PROGRESS = process.env.HERMES_FAKE_PROGRESS || ''
|
||||
const PIDFILE = process.env.HERMES_FAKE_PIDFILE || ''
|
||||
const DIE_FLAG = process.env.HERMES_FAKE_DIE_FLAG || ''
|
||||
const SUBMIT_RESPONSE = process.env.HERMES_FAKE_SUBMIT_RESPONSE === '1'
|
||||
|
||||
// Chaos self-termination (deterministic, no external kill races). Die-once:
|
||||
// if the flag file exists a previous instance already died here — this is the
|
||||
// auto-heal respawn, which must stream to completion.
|
||||
let dieAtMsgs = null
|
||||
let dieKind = 'kill'
|
||||
{
|
||||
const m = (process.env.HERMES_FAKE_DIE_AT || '').match(/^(\d+):(kill|tool-kill)$/)
|
||||
if (m) {
|
||||
dieAtMsgs = Number(m[1])
|
||||
dieKind = m[2]
|
||||
}
|
||||
if (dieAtMsgs !== null && DIE_FLAG && existsSync(DIE_FLAG)) dieAtMsgs = null
|
||||
}
|
||||
|
||||
if (PIDFILE) {
|
||||
try {
|
||||
writeFileSync(PIDFILE, String(process.pid))
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
|
||||
const t0 = Date.now()
|
||||
const progress = obj => {
|
||||
if (!PROGRESS) return
|
||||
try {
|
||||
appendFileSync(PROGRESS, JSON.stringify({ ...obj, t: Date.now() - t0, wall: Date.now() }) + '\n')
|
||||
} catch {
|
||||
/* progress is best-effort; never crash the wire */
|
||||
}
|
||||
}
|
||||
|
||||
// UI gone (pipe closed) → exit quietly like the real child on stdin EOF.
|
||||
process.stdout.on('error', () => process.exit(0))
|
||||
|
||||
const writeFrame = obj => {
|
||||
const ok = process.stdout.write(JSON.stringify(obj) + '\n')
|
||||
return ok ? null : new Promise(r => process.stdout.once('drain', r))
|
||||
}
|
||||
const emitEvent = params => writeFrame({ jsonrpc: '2.0', method: 'event', params })
|
||||
|
||||
// ── Canned RPC results (recon'd from both UIs' startup sequences) ──────
|
||||
const SESSION_ID = 'bench-session-0001'
|
||||
const INFO = {
|
||||
model: 'bench/fake-model',
|
||||
version: '0.0.0-bench',
|
||||
cwd: process.env.HERMES_CWD || process.cwd(),
|
||||
skills: {},
|
||||
tools: { core: ['terminal', 'read_file'] },
|
||||
usage: { calls: 0, input: 0, output: 0, total: 0 }
|
||||
}
|
||||
|
||||
function resultFor(method, params) {
|
||||
switch (method) {
|
||||
case 'setup.status':
|
||||
return { provider_configured: true }
|
||||
case 'session.create':
|
||||
return { session_id: SESSION_ID, info: INFO }
|
||||
case 'session.resume':
|
||||
case 'session.activate':
|
||||
return { session_id: SESSION_ID, messages: [], info: INFO }
|
||||
case 'session.most_recent':
|
||||
return {}
|
||||
case 'session.list':
|
||||
case 'session.active_list':
|
||||
return { sessions: [] }
|
||||
case 'config.get':
|
||||
if (params && params.key === 'mtime') return { mtime: 1 }
|
||||
if (params && params.key === 'full') return { config: { display: {} } }
|
||||
return { value: '' }
|
||||
case 'commands.catalog':
|
||||
return { pairs: [['help', 'show help']], canon: {}, categories: [], sub: {}, skill_count: 0 }
|
||||
case 'startup.catalog':
|
||||
return { tools: {}, skills: {}, mcp_servers: [] }
|
||||
case 'model.options':
|
||||
return { providers: [] }
|
||||
case 'session.title':
|
||||
return { title: 'bench' }
|
||||
case 'prompt.submit':
|
||||
return { ok: true }
|
||||
case 'session.interrupt':
|
||||
return { ok: true }
|
||||
case 'complete.slash':
|
||||
case 'complete.path':
|
||||
return { items: [] }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Chaos self-kill ────────────────────────────────────────────────────
|
||||
// Flag first (sync — survives SIGKILL), then a 'dying' progress line (gives
|
||||
// the harness the precise kill wall-clock), then SIGKILL self.
|
||||
function dieNow(msgs) {
|
||||
if (DIE_FLAG) {
|
||||
try {
|
||||
writeFileSync(DIE_FLAG, '1')
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
progress({ k: 'dying', kind: dieKind, msgs })
|
||||
process.kill(process.pid, 'SIGKILL')
|
||||
}
|
||||
|
||||
// ── Fixture streaming ──────────────────────────────────────────────────
|
||||
let streaming = false
|
||||
async function streamFixture() {
|
||||
if (streaming || !FIXTURE) return
|
||||
streaming = true
|
||||
const lines = readFileSync(FIXTURE, 'utf8').split('\n')
|
||||
let msgs = 0
|
||||
let events = 0
|
||||
let nextBoundary = SAMPLE_EVERY
|
||||
const paced = MODE === 'paced'
|
||||
const interval = paced ? 1000 / RATE : 0
|
||||
let nextAt = Date.now()
|
||||
progress({ k: 'stream_start', mode: MODE })
|
||||
for (const raw of lines) {
|
||||
if (!raw) continue
|
||||
const item = JSON.parse(raw)
|
||||
if (item.k === 'e') {
|
||||
if (paced) {
|
||||
const wait = nextAt - Date.now()
|
||||
if (wait > 0) await new Promise(r => setTimeout(r, wait))
|
||||
nextAt += interval
|
||||
}
|
||||
const drained = emitEvent(item.v)
|
||||
if (drained) await drained
|
||||
events++
|
||||
// tool-kill: die exactly as a tool-call event goes over the wire (the
|
||||
// first tool.* event after the armed msg index — the UI is left with a
|
||||
// started, never-completed tool).
|
||||
if (dieAtMsgs !== null && dieKind === 'tool-kill' && msgs >= dieAtMsgs && typeof item.v?.type === 'string' && item.v.type.startsWith('tool.')) {
|
||||
dieNow(msgs)
|
||||
}
|
||||
} else if (item.k === 't') {
|
||||
msgs = item.msgs
|
||||
if (msgs >= nextBoundary) {
|
||||
progress({ k: 'boundary', msgs, events })
|
||||
while (nextBoundary <= msgs) nextBoundary += SAMPLE_EVERY
|
||||
}
|
||||
if (dieAtMsgs !== null && dieKind === 'kill' && msgs >= dieAtMsgs) dieNow(msgs)
|
||||
}
|
||||
// {"k":"r"} row markers: composer-local rows, nothing on the wire.
|
||||
}
|
||||
progress({ k: 'done', msgs, events })
|
||||
}
|
||||
|
||||
// ── Main: handshake + request loop ─────────────────────────────────────
|
||||
progress({ k: 'start', pid: process.pid, mode: MODE, fixture: FIXTURE })
|
||||
emitEvent({ type: 'gateway.ready', payload: { skin: {} } })
|
||||
|
||||
const rl = createInterface({ input: process.stdin })
|
||||
rl.on('line', line => {
|
||||
let msg
|
||||
try {
|
||||
msg = JSON.parse(line)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!msg || typeof msg !== 'object' || msg.id === undefined) return
|
||||
const method = String(msg.method ?? '')
|
||||
progress({ k: 'req', method })
|
||||
void writeFrame({ jsonrpc: '2.0', id: msg.id, result: resultFor(method, msg.params) })
|
||||
if (method === 'session.create' || method === 'session.resume') {
|
||||
setTimeout(() => {
|
||||
streamFixture().catch(() => process.exit(1))
|
||||
}, START_DELAY_MS)
|
||||
}
|
||||
// Echo cells: a real (tiny) reply to prompt.submit so input→first-token-paint
|
||||
// is measurable. The marker token "zqxjv" never occurs in the lorem fixture.
|
||||
if (method === 'prompt.submit' && SUBMIT_RESPONSE) {
|
||||
setTimeout(() => {
|
||||
progress({ k: 'submit_response' })
|
||||
void emitEvent({ type: 'message.start' })
|
||||
void emitEvent({ type: 'message.delta', payload: { text: 'Echo probe reply zqxjv — bench token-paint marker.' } })
|
||||
void emitEvent({ type: 'message.complete' })
|
||||
}, 30)
|
||||
}
|
||||
})
|
||||
rl.on('close', () => {
|
||||
progress({ k: 'eof' })
|
||||
process.exit(0)
|
||||
})
|
||||
86
bench/fixture-stream.mjs
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
// Serialize the deterministic lumpy-turn fixture (ui-opentui/scripts/fixture.ts)
|
||||
// to NDJSON for the fake gateway. We check in THIS generator invocation, not the
|
||||
// generated file (it is megabytes); the stream is byte-reproducible for a given
|
||||
// message count because the fixture is seeded by turn index.
|
||||
//
|
||||
// The generator is imported DIRECTLY from ui-opentui/scripts/fixture.ts via
|
||||
// Node >=26 type stripping — no port, no drift. `applyTurn(store, turn)` only
|
||||
// calls store.pushUser/pushSystem/apply, so a recorder stub extracts the exact
|
||||
// per-turn action stream the OpenTUI mem-bench drives.
|
||||
//
|
||||
// Line format (one JSON object per line):
|
||||
// {"k":"e","v":{...GatewayEvent...}} → sent on the wire as
|
||||
// {jsonrpc:"2.0",method:"event",params:v}
|
||||
// {"k":"r","role":"user"|"system"} → row marker, NOT sent (composer-local
|
||||
// rows have no wire representation —
|
||||
// see README "deviation: user rows")
|
||||
// {"k":"t","msgs":N} → end-of-turn marker with the CUMULATIVE
|
||||
// fixture-message count (rowsPerTurn
|
||||
// accounting, same as scripts/mem-bench.tsx)
|
||||
//
|
||||
// Usage: node fixture-stream.mjs --msgs 3000 [--out path]
|
||||
// Default out: bench/.cache/fixture-<msgs>.ndjson (prints path + sha256)
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createWriteStream, mkdirSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const fixtureTs = resolve(here, '../ui-opentui/scripts/fixture.ts')
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { msgs: 3000, out: null }
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
if (argv[i] === '--msgs') args.msgs = Number.parseInt(argv[++i], 10)
|
||||
else if (argv[i] === '--out') args.out = argv[++i]
|
||||
}
|
||||
if (!Number.isFinite(args.msgs) || args.msgs <= 0) throw new Error('--msgs must be a positive integer')
|
||||
return args
|
||||
}
|
||||
|
||||
export async function generate(msgs, outPath) {
|
||||
const { applyTurn, rowsPerTurn } = await import(pathToFileURL(fixtureTs).href)
|
||||
mkdirSync(dirname(outPath), { recursive: true })
|
||||
const out = createWriteStream(outPath)
|
||||
const hash = createHash('sha256')
|
||||
const write = line => {
|
||||
const data = line + '\n'
|
||||
hash.update(data)
|
||||
if (!out.write(data)) return new Promise(r => out.once('drain', r))
|
||||
return null
|
||||
}
|
||||
|
||||
let pushed = 0
|
||||
let events = 0
|
||||
let turn = 0
|
||||
while (pushed < msgs) {
|
||||
const lines = []
|
||||
const recorder = {
|
||||
pushUser: () => lines.push('{"k":"r","role":"user"}'),
|
||||
pushSystem: () => lines.push('{"k":"r","role":"system"}'),
|
||||
apply: ev => {
|
||||
lines.push(JSON.stringify({ k: 'e', v: ev }))
|
||||
events++
|
||||
}
|
||||
}
|
||||
applyTurn(recorder, turn)
|
||||
pushed += rowsPerTurn(turn)
|
||||
lines.push(JSON.stringify({ k: 't', msgs: pushed }))
|
||||
for (const line of lines) {
|
||||
const wait = write(line)
|
||||
if (wait) await wait
|
||||
}
|
||||
turn++
|
||||
}
|
||||
await new Promise((res, rej) => out.end(err => (err ? rej(err) : res())))
|
||||
return { path: outPath, msgs: pushed, events, turns: turn, sha256: hash.digest('hex') }
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const args = parseArgs(process.argv)
|
||||
const outPath = args.out ?? resolve(here, `.cache/fixture-${args.msgs}.ndjson`)
|
||||
const info = await generate(args.msgs, outPath)
|
||||
process.stdout.write(JSON.stringify(info) + '\n')
|
||||
}
|
||||
371
bench/forensics.sh
Executable file
@@ -0,0 +1,371 @@
|
||||
#!/usr/bin/env bash
|
||||
# forensics.sh — assemble a chronological "what killed my gateway" timeline.
|
||||
#
|
||||
# Usage: bench/forensics.sh <since>
|
||||
# <since> is anything `date -d` accepts: '3 days ago', '2026-06-08', 'yesterday' ...
|
||||
#
|
||||
# Read-only against system state: it greps logs, queries the sessions DB via a
|
||||
# read-only sqlite URI, reads journalctl/dmesg, lists worktrees and running
|
||||
# processes. It never kills, restarts or writes anything outside mktemp.
|
||||
#
|
||||
# Sources merged into one timestamp-sorted timeline (local time, ISO):
|
||||
# [gateway.log] ~/.hermes/logs/gateway.log* gateway lifecycle lines
|
||||
# [errors.log] ~/.hermes/logs/errors.log* ERROR/CRITICAL lines
|
||||
# [exit-diag] ~/.hermes/logs/gateway-exit-diag.log (JSONL, UTC)
|
||||
# [tui-crash] ~/.hermes/logs/tui_gateway_crash.log exit/signal/exception markers
|
||||
# [opentui] ~/.hermes/logs/opentui-v2.log (JSONL, epoch ms)
|
||||
# [shutdown-diag] ~/.hermes/logs/gateway-shutdown-diag.log SIGTERM dump headers
|
||||
# [oom]/[systemd]/[sleep] journalctl --user / -k (dmesg fallback)
|
||||
# [sessions] ~/.hermes/state.db sessions table (tui/cli sources)
|
||||
# [worktree] git worktree lists + dir mtimes under ~/github
|
||||
# [proc] currently running tui_gateway / dist/main.js / dist/entry.js
|
||||
set -uo pipefail
|
||||
|
||||
SINCE_SPEC="${1:-}"
|
||||
if [ -z "$SINCE_SPEC" ]; then
|
||||
echo "usage: $0 <since> (e.g. '3 days ago', '2026-06-08')" >&2
|
||||
exit 2
|
||||
fi
|
||||
SINCE_EPOCH="$(date -d "$SINCE_SPEC" +%s 2>/dev/null)" || {
|
||||
echo "error: date -d could not parse: $SINCE_SPEC" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PY="$REPO_ROOT/.venv/bin/python"
|
||||
[ -x "$PY" ] || PY="$(command -v python3)"
|
||||
|
||||
TMP="$(mktemp -d /tmp/forensics.XXXXXX)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# ---------------------------------------------------------------- journal ---
|
||||
# User journal: OOM notices, hermes-gateway unit lifecycle, suspend/resume.
|
||||
journalctl --user --since "@$SINCE_EPOCH" -o short-iso-precise --no-pager 2>"$TMP/jr-user.err" \
|
||||
| grep -iE 'oom|out of memory|killed process|hermes-gateway[^ ]*\.service|suspend|hibernat|Scheduled restart' \
|
||||
> "$TMP/journal-user.txt" || true
|
||||
|
||||
# Kernel journal: the authoritative OOM-kill records.
|
||||
if ! journalctl -k --since "@$SINCE_EPOCH" -o short-iso-precise --no-pager 2>"$TMP/jr-kern.err" \
|
||||
| grep -iE 'out of memory|oom-kill|oom_reaper|invoked oom-killer' \
|
||||
> "$TMP/journal-kernel.txt"; then
|
||||
: > "$TMP/journal-kernel.txt"
|
||||
fi
|
||||
if [ -s "$TMP/jr-kern.err" ] && [ ! -s "$TMP/journal-kernel.txt" ]; then
|
||||
echo "note: journalctl -k unavailable ($(head -1 "$TMP/jr-kern.err")); trying dmesg" >&2
|
||||
dmesg -T 2>/dev/null | grep -iE 'out of memory|oom-kill|invoked oom-killer' > "$TMP/dmesg.txt" || true
|
||||
fi
|
||||
[ -e "$TMP/dmesg.txt" ] || : > "$TMP/dmesg.txt"
|
||||
|
||||
# ------------------------------------------------------------- processes ---
|
||||
ps -eo pid,lstart,rss,args --sort=lstart 2>/dev/null \
|
||||
| grep -E 'tui_gateway|dist/main\.js|dist/entry\.js' \
|
||||
| grep -vE 'grep|forensics' > "$TMP/ps.txt" || true
|
||||
|
||||
# -------------------------------------------------------------- worktrees ---
|
||||
{
|
||||
for d in "$HOME"/github/*/; do
|
||||
[ -e "$d/.git" ] || continue
|
||||
echo "## repo $d"
|
||||
timeout 10 git -C "$d" worktree list 2>/dev/null || echo "(git worktree list failed)"
|
||||
done
|
||||
} > "$TMP/worktrees.txt" 2>/dev/null || true
|
||||
|
||||
# ---------------------------------------------------------------- python ---
|
||||
export FORENSICS_SINCE="$SINCE_EPOCH" FORENSICS_TMP="$TMP" FORENSICS_SINCE_SPEC="$SINCE_SPEC"
|
||||
exec "$PY" - <<'PYEOF'
|
||||
import json, os, re, sqlite3, sys, time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
SINCE = float(os.environ["FORENSICS_SINCE"])
|
||||
TMP = os.environ["FORENSICS_TMP"]
|
||||
NOW = time.time()
|
||||
HOME = os.path.expanduser("~")
|
||||
LOGS = os.path.join(HOME, ".hermes", "logs")
|
||||
|
||||
events = [] # (epoch, tag, msg)
|
||||
|
||||
def add(ep, tag, msg):
|
||||
if ep is None or ep < SINCE or ep > NOW + 120:
|
||||
return
|
||||
msg = " ".join(str(msg).split())
|
||||
if msg:
|
||||
events.append((ep, tag, msg[:500]))
|
||||
|
||||
def local_naive(s, fmt):
|
||||
"""Parse a naive local-time string -> epoch."""
|
||||
try:
|
||||
return datetime.strptime(s, fmt).timestamp()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def iso_any(s):
|
||||
"""Parse an ISO timestamp (Z / +00:00 / +0530 offsets) -> epoch."""
|
||||
s = s.strip().replace("Z", "+00:00")
|
||||
# journald short-iso uses +0530 (no colon); fromisoformat on 3.11+ copes.
|
||||
try:
|
||||
return datetime.fromisoformat(s).timestamp()
|
||||
except ValueError:
|
||||
m = re.match(r"(.*)([+-]\d{2})(\d{2})$", s)
|
||||
if m:
|
||||
try:
|
||||
return datetime.fromisoformat(f"{m.group(1)}{m.group(2)}:{m.group(3)}").timestamp()
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def read_lines(path):
|
||||
try:
|
||||
with open(path, errors="replace") as f:
|
||||
return f.readlines()
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
PYLOG = re.compile(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+\s+(\w+)\s+(.*)$")
|
||||
|
||||
# --- gateway.log* : lifecycle lines -----------------------------------------
|
||||
LIFECYCLE = re.compile(
|
||||
r"Starting Hermes Gateway|Gateway running|Press Ctrl\+C|Shutting down|shutdown"
|
||||
r"|stopp(ed|ing)|Recovered \d+ background|reaped|restart|Cron ticker started",
|
||||
re.I,
|
||||
)
|
||||
for path in sorted(p for p in os.listdir(LOGS) if p.startswith("gateway.log")):
|
||||
for line in read_lines(os.path.join(LOGS, path)):
|
||||
m = PYLOG.match(line)
|
||||
if m and LIFECYCLE.search(m.group(3)):
|
||||
add(local_naive(m.group(1), "%Y-%m-%d %H:%M:%S"), "gateway.log", m.group(3))
|
||||
|
||||
# --- errors.log* : ERROR/CRITICAL header lines ------------------------------
|
||||
for path in sorted(p for p in os.listdir(LOGS) if p.startswith("errors.log")):
|
||||
for line in read_lines(os.path.join(LOGS, path)):
|
||||
m = PYLOG.match(line)
|
||||
if m and m.group(2) in ("ERROR", "CRITICAL"):
|
||||
add(local_naive(m.group(1), "%Y-%m-%d %H:%M:%S"), "errors.log",
|
||||
f"{m.group(2)} {m.group(3)}")
|
||||
|
||||
# --- gateway-exit-diag.log : JSONL, UTC ISO ---------------------------------
|
||||
for line in read_lines(os.path.join(LOGS, "gateway-exit-diag.log")):
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
continue
|
||||
tag = rec.get("tag", "?")
|
||||
extra = ""
|
||||
if tag == "asyncio.run.SystemExit":
|
||||
extra = f" code={rec.get('code')}"
|
||||
elif tag == "gateway.start":
|
||||
extra = f" replace={rec.get('replace')} argv={' '.join(rec.get('argv', [])[-3:])}"
|
||||
elif tag == "asyncio.run.returned":
|
||||
extra = f" success={rec.get('success')}"
|
||||
add(iso_any(rec.get("ts", "")), "exit-diag", f"{tag} pid={rec.get('pid')}{extra}")
|
||||
|
||||
# --- tui_gateway_crash.log : section markers + [tui-parent] lines -----------
|
||||
SECTION = re.compile(r"^=== (.+?) · (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})(?: · (.*?))? ===\s*$")
|
||||
TUIPARENT = re.compile(r"^\[tui-parent\] (\S+Z) (.*)$")
|
||||
for line in read_lines(os.path.join(LOGS, "tui_gateway_crash.log")):
|
||||
m = SECTION.match(line)
|
||||
if m:
|
||||
what, ts, detail = m.group(1), m.group(2), m.group(3) or ""
|
||||
add(local_naive(ts, "%Y-%m-%d %H:%M:%S"), "tui-crash",
|
||||
f"{what}{' · ' + detail if detail else ''}")
|
||||
continue
|
||||
m = TUIPARENT.match(line)
|
||||
if m and ("[lifecycle]" in m.group(2) or "uncaughtException" in m.group(2)):
|
||||
add(iso_any(m.group(1)), "tui-parent", m.group(2))
|
||||
|
||||
# --- opentui-v2.log : JSONL, epoch ms ---------------------------------------
|
||||
for line in read_lines(os.path.join(LOGS, "opentui-v2.log")):
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
continue
|
||||
keep = (rec.get("scope") == "gateway"
|
||||
or rec.get("level") in ("warn", "error")
|
||||
or "transport" in str(rec.get("msg", "")))
|
||||
if keep:
|
||||
data = rec.get("data") or {}
|
||||
brief = {k: v for k, v in data.items() if k in
|
||||
("python", "reason", "code", "signal", "cause", "sid", "attempt")}
|
||||
add(rec.get("t", 0) / 1000.0, "opentui",
|
||||
f"{rec.get('level')} {rec.get('scope')}: {rec.get('msg')} {brief if brief else ''}")
|
||||
|
||||
# --- gateway-shutdown-diag.log : SIGTERM dump headers -----------------------
|
||||
lines = read_lines(os.path.join(LOGS, "gateway-shutdown-diag.log"))
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("=== shutdown diagnostic"):
|
||||
for j in range(i, min(i + 4, len(lines))):
|
||||
mm = re.match(r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s*$", lines[j])
|
||||
if mm:
|
||||
add(iso_any(mm.group(1)), "shutdown-diag",
|
||||
line.strip().strip("= ").strip())
|
||||
break
|
||||
|
||||
# --- journal files ----------------------------------------------------------
|
||||
JLINE = re.compile(r"^(\S+)\s+\S+\s+(.*)$")
|
||||
def journal(path, default_tag):
|
||||
for line in read_lines(path):
|
||||
m = JLINE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
ep, msg = iso_any(m.group(1)), m.group(2)
|
||||
low = msg.lower()
|
||||
if "out of memory" in low or "oom-kill" in low or "oom killer" in low \
|
||||
or "invoked oom-killer" in low or "result 'oom-kill'" in low:
|
||||
tag = "oom"
|
||||
elif "suspend" in low or "hibernat" in low:
|
||||
tag = "sleep"
|
||||
else:
|
||||
tag = default_tag
|
||||
add(ep, tag, msg)
|
||||
|
||||
journal(os.path.join(TMP, "journal-user.txt"), "systemd")
|
||||
journal(os.path.join(TMP, "journal-kernel.txt"), "oom")
|
||||
for line in read_lines(os.path.join(TMP, "dmesg.txt")):
|
||||
m = re.match(r"^\[(\w{3} \w{3} +\d+ \d{2}:\d{2}:\d{2} \d{4})\]\s*(.*)$", line)
|
||||
if m:
|
||||
add(local_naive(re.sub(r" +", " ", m.group(1)), "%a %b %d %H:%M:%S %Y"),
|
||||
"oom", m.group(2))
|
||||
|
||||
# --- sessions DB ------------------------------------------------------------
|
||||
abnormal_sessions = []
|
||||
db = os.path.join(HOME, ".hermes", "state.db")
|
||||
try:
|
||||
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=5)
|
||||
rows = con.execute(
|
||||
"SELECT id, source, started_at, ended_at, end_reason, message_count "
|
||||
"FROM sessions WHERE source IN ('tui','cli') AND "
|
||||
"(started_at >= ? OR (ended_at IS NOT NULL AND ended_at >= ?)) "
|
||||
"ORDER BY started_at", (SINCE, SINCE)).fetchall()
|
||||
con.close()
|
||||
for sid, source, st, en, reason, mc in rows:
|
||||
add(st, "sessions", f"START {source} session={sid} messages={mc}")
|
||||
if en is not None:
|
||||
flag = "" if reason else " ABNORMAL(no end_reason)"
|
||||
add(en, "sessions",
|
||||
f"END {source} session={sid} reason={reason or 'NULL'} messages={mc}{flag}")
|
||||
if not reason:
|
||||
abnormal_sessions.append((sid, source, st, "ended, no end_reason"))
|
||||
else:
|
||||
add(st, "sessions",
|
||||
f"NOEND {source} session={sid} messages={mc} "
|
||||
f"ABNORMAL(no ended_at recorded — crashed parent or still running)")
|
||||
abnormal_sessions.append((sid, source, st, "no ended_at"))
|
||||
except sqlite3.Error as e:
|
||||
print(f"note: sessions DB unreadable: {e}", file=sys.stderr)
|
||||
|
||||
# --- worktrees: current list + dir mtimes -----------------------------------
|
||||
worktree_snapshot = open(os.path.join(TMP, "worktrees.txt"), errors="replace").read() \
|
||||
if os.path.exists(os.path.join(TMP, "worktrees.txt")) else ""
|
||||
wt_dirs = []
|
||||
for base in ([os.path.join(HOME, "github", d, ".worktrees")
|
||||
for d in (os.listdir(os.path.join(HOME, "github"))
|
||||
if os.path.isdir(os.path.join(HOME, "github")) else [])]
|
||||
+ [os.path.join(HOME, "github", "worktrees", d)
|
||||
for d in (os.listdir(os.path.join(HOME, "github", "worktrees"))
|
||||
if os.path.isdir(os.path.join(HOME, "github", "worktrees")) else [])]):
|
||||
if not os.path.isdir(base):
|
||||
continue
|
||||
for name in os.listdir(base):
|
||||
p = os.path.join(base, name)
|
||||
if os.path.isdir(p):
|
||||
try:
|
||||
mt = os.stat(p).st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
wt_dirs.append((p, mt))
|
||||
add(mt, "worktree", f"last-modified {p} "
|
||||
f"(age {round((NOW - mt) / 3600, 1)}h)")
|
||||
|
||||
# --- process snapshot -------------------------------------------------------
|
||||
running = []
|
||||
PSLINE = re.compile(r"^\s*(\d+)\s+(\w{3} \w{3} +\d+ \d{2}:\d{2}:\d{2} \d{4})\s+(\d+)\s+(.*)$")
|
||||
for line in read_lines(os.path.join(TMP, "ps.txt")):
|
||||
m = PSLINE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
pid, lstart, rss, args = m.groups()
|
||||
ep = local_naive(re.sub(r" +", " ", lstart), "%a %b %d %H:%M:%S %Y")
|
||||
running.append((pid, ep, int(rss), args))
|
||||
add(ep, "proc", f"STILL-RUNNING pid={pid} rss={int(rss)//1024}MB started-here: {args[:200]}")
|
||||
|
||||
# --- emit timeline ----------------------------------------------------------
|
||||
def iso(ep):
|
||||
return datetime.fromtimestamp(ep).astimezone().strftime("%Y-%m-%dT%H:%M:%S%z")
|
||||
|
||||
print(f"# forensics timeline since {os.environ['FORENSICS_SINCE_SPEC']!r} "
|
||||
f"({iso(SINCE)}) — generated {iso(NOW)}")
|
||||
print(f"# {len(events)} events\n")
|
||||
|
||||
events.sort(key=lambda e: e[0])
|
||||
prev = None
|
||||
dup = 0
|
||||
def flush(prev, dup):
|
||||
if prev is None:
|
||||
return
|
||||
suffix = f" (x{dup + 1})" if dup else ""
|
||||
print(f"{iso(prev[0])} [{prev[1]}] {prev[2]}{suffix}")
|
||||
for ev in events:
|
||||
if prev and ev[1] == prev[1] and ev[2] == prev[2] and ev[0] - prev[0] < 5:
|
||||
dup += 1
|
||||
continue
|
||||
flush(prev, dup)
|
||||
prev, dup = ev, 0
|
||||
flush(prev, dup)
|
||||
|
||||
# --- summary ----------------------------------------------------------------
|
||||
print("\n" + "=" * 72)
|
||||
print("SUMMARY")
|
||||
print("=" * 72)
|
||||
from collections import Counter
|
||||
by_tag = Counter(e[1] for e in events)
|
||||
for tag, n in by_tag.most_common():
|
||||
print(f" {n:6d} [{tag}]")
|
||||
|
||||
ooms = [e for e in events if e[1] == "oom" and "Killed process" in e[2]]
|
||||
print(f"\nOOM kernel kills in window: {len(ooms)}")
|
||||
for e in ooms:
|
||||
m = re.search(r"Killed process (\d+) \(([^)]+)\).*?anon-rss:(\d+)kB", e[2])
|
||||
if m:
|
||||
print(f" {iso(e[0])} pid={m.group(1)} comm={m.group(2)} anon-rss={int(m.group(3))//1024}MB")
|
||||
else:
|
||||
print(f" {iso(e[0])} {e[2][:140]}")
|
||||
|
||||
oomd = [e for e in events if e[1] == "oom" and "Killed process" not in e[2]
|
||||
and ("oom" in e[2].lower())]
|
||||
print(f"OOM-related systemd/unit notices: {len(oomd)}")
|
||||
|
||||
gexits = Counter()
|
||||
for e in events:
|
||||
if e[1] == "tui-crash" and e[2].startswith("gateway exit"):
|
||||
m = re.search(r"reason=(.*)$", e[2])
|
||||
gexits[m.group(1) if m else "?"] += 1
|
||||
print(f"\ntui_gateway exits by reason (tui_gateway_crash.log):")
|
||||
for r, n in gexits.most_common():
|
||||
print(f" {n:4d} {r}")
|
||||
|
||||
sigs = Counter(e[2].split(" received")[0] for e in events
|
||||
if e[1] == "tui-crash" and " received" in e[2])
|
||||
print(f"tui_gateway signals received: {dict(sigs) if sigs else 'none'}")
|
||||
|
||||
starts = sum(1 for e in events if e[1] == "exit-diag" and e[2].startswith("gateway.start"))
|
||||
nz = sum(1 for e in events if e[1] == "exit-diag" and "exit_nonzero" in e[2])
|
||||
print(f"\nplatform gateway (hermes-gateway.service): {starts} start(s), {nz} nonzero-exit(s) in window")
|
||||
|
||||
print(f"\nabnormal tui/cli sessions (no ended_at or no end_reason): {len(abnormal_sessions)}")
|
||||
for sid, source, st, why in abnormal_sessions[-20:]:
|
||||
print(f" {iso(st)} {source} {sid}: {why}")
|
||||
|
||||
sleeps = [e for e in events if e[1] == "sleep"]
|
||||
print(f"\nsuspend/hibernate events: {len(sleeps)}")
|
||||
|
||||
print(f"\ncurrently running TUI/gateway processes: {len(running)}")
|
||||
for pid, ep, rss, args in running:
|
||||
print(f" pid={pid} since={iso(ep) if ep else '?'} rss={rss//1024}MB {args[:120]}")
|
||||
|
||||
print(f"\ncurrent git worktrees (snapshot, not historical):")
|
||||
for line in worktree_snapshot.splitlines():
|
||||
print(f" {line}")
|
||||
print("\nNOTE: worktree DELETIONS leave no on-disk record; only surviving dirs are")
|
||||
print("listed. Prune suspects: cli.py _prune_stale_worktrees (24h/72h tiers) and")
|
||||
print("the atexit _cleanup_worktree hook (removes dirty worktrees w/o unpushed commits).")
|
||||
PYEOF
|
||||
1301
bench/harness.mjs
Normal file
59
bench/live-attach.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# live-attach.sh — plug into a RUNNING hermes TUI (Ink or OpenTUI) and measure it.
|
||||
#
|
||||
# bench/live-attach.sh <pid> [out-dir] # sample memory+cpu until Ctrl-C
|
||||
# bench/live-attach.sh <pid> --profile [secs] # also grab a CPU profile window (default 30s)
|
||||
# bench/live-attach.sh <pid> --heap # grab a heap snapshot (large file!)
|
||||
#
|
||||
# Find your TUI pid: pgrep -af 'dist/main.js' (OpenTUI)
|
||||
# pgrep -af 'dist/entry.js' (Ink)
|
||||
# Works on any live session — no restart, no flags needed at launch:
|
||||
# profiling uses SIGUSR1 (Node opens an inspector port on demand).
|
||||
# In-TUI complements (OpenTUI only): /mem (live stats line), /heapdump.
|
||||
set -euo pipefail
|
||||
PID="${1:?usage: live-attach.sh <pid> [outdir|--profile [secs]|--heap]}"
|
||||
shift || true
|
||||
OUT="${1:-/tmp/tui-live-$PID}"; MODE="sample"; SECS=30
|
||||
[[ "${1:-}" == "--profile" ]] && { MODE=profile; OUT="/tmp/tui-live-$PID"; SECS="${2:-30}"; }
|
||||
[[ "${1:-}" == "--heap" ]] && { MODE=heap; OUT="/tmp/tui-live-$PID"; }
|
||||
mkdir -p "$OUT"
|
||||
echo "target pid=$PID cmd=$(tr '\0' ' ' </proc/$PID/cmdline | cut -c1-80)"
|
||||
echo "out: $OUT"
|
||||
|
||||
sample() {
|
||||
local f="$OUT/samples.jsonl"
|
||||
echo "sampling 1Hz → $f (Ctrl-C to stop; render: node bench/live-render.mjs $OUT)"
|
||||
local prev_cpu=0 hz; hz=$(getconf CLK_TCK)
|
||||
while kill -0 "$PID" 2>/dev/null; do
|
||||
local rss pss pdirty hwm cpu t
|
||||
rss=$(awk '/^Rss:/{print $2}' /proc/$PID/smaps_rollup 2>/dev/null || echo 0)
|
||||
pss=$(awk '/^Pss:/{print $2}' /proc/$PID/smaps_rollup 2>/dev/null || echo 0)
|
||||
pdirty=$(awk '/^Private_Dirty:/{print $2}' /proc/$PID/smaps_rollup 2>/dev/null || echo 0)
|
||||
hwm=$(awk '/^VmHWM:/{print $2}' /proc/$PID/status 2>/dev/null || echo 0)
|
||||
cpu=$(awk '{print $14+$15}' /proc/$PID/stat 2>/dev/null || echo 0)
|
||||
t=$(date +%s.%N)
|
||||
printf '{"t":%s,"rss_kb":%s,"pss_kb":%s,"private_dirty_kb":%s,"vmhwm_kb":%s,"cpu_ticks":%s,"cpu_hz":%s}\n' \
|
||||
"$t" "$rss" "$pss" "$pdirty" "$hwm" "$cpu" "$hz" >> "$f"
|
||||
sleep 1
|
||||
done
|
||||
echo "process exited; $(wc -l <"$f") samples in $f"
|
||||
}
|
||||
|
||||
cdp() { # open inspector on demand, find the ws url
|
||||
kill -USR1 "$PID"; sleep 0.7
|
||||
local port; port=$(ss -tlnp 2>/dev/null | grep "pid=$PID" | grep -oE ':(92[0-9]{2})' | head -1 | tr -d ':')
|
||||
[[ -z "$port" ]] && port=9229
|
||||
curl -s "http://127.0.0.1:$port/json" | grep -oE 'ws://[^"]+' | head -1
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
sample) sample ;;
|
||||
profile)
|
||||
WS=$(cdp); echo "CDP: $WS — profiling ${SECS}s (interact with the TUI now!)"
|
||||
node "$(dirname "$0")/live-cdp.mjs" "$WS" profile "$SECS" "$OUT/live.cpuprofile"
|
||||
echo "→ $OUT/live.cpuprofile (open in https://speedscope.app or chrome://inspect)" ;;
|
||||
heap)
|
||||
WS=$(cdp); echo "CDP: $WS — heap snapshot (may pause the TUI briefly)"
|
||||
node "$(dirname "$0")/live-cdp.mjs" "$WS" heap 0 "$OUT/live.heapsnapshot"
|
||||
echo "→ $OUT/live.heapsnapshot (Chrome DevTools → Memory → Load)" ;;
|
||||
esac
|
||||
44
bench/live-cdp.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
// live-cdp.mjs — minimal CDP client for live-attach.sh (no deps; Node ws via raw socket
|
||||
// is overkill — use the built-in WebSocket of Node >=22).
|
||||
// usage: node live-cdp.mjs <ws-url> profile <secs> <out> | heap 0 <out>
|
||||
const [, , url, mode, secsArg, out] = process.argv
|
||||
const { writeFileSync, appendFileSync } = await import('node:fs')
|
||||
const ws = new WebSocket(url)
|
||||
let id = 0
|
||||
const pending = new Map()
|
||||
const send = (method, params = {}) =>
|
||||
new Promise((res, rej) => {
|
||||
const i = ++id
|
||||
pending.set(i, { res, rej })
|
||||
ws.send(JSON.stringify({ id: i, method, params }))
|
||||
})
|
||||
const chunks = []
|
||||
ws.onmessage = e => {
|
||||
const m = JSON.parse(e.data)
|
||||
if (m.id && pending.has(m.id)) {
|
||||
const { res, rej } = pending.get(m.id)
|
||||
pending.delete(m.id)
|
||||
m.error ? rej(new Error(m.error.message)) : res(m.result)
|
||||
} else if (m.method === 'HeapProfiler.addHeapSnapshotChunk') chunks.push(m.params.chunk)
|
||||
}
|
||||
ws.onopen = async () => {
|
||||
try {
|
||||
if (mode === 'profile') {
|
||||
await send('Profiler.enable')
|
||||
await send('Profiler.start')
|
||||
await new Promise(r => setTimeout(r, Number(secsArg) * 1000))
|
||||
const { profile } = await send('Profiler.stop')
|
||||
writeFileSync(out, JSON.stringify(profile))
|
||||
} else {
|
||||
await send('HeapProfiler.enable')
|
||||
await send('HeapProfiler.takeHeapSnapshot', { reportProgress: false })
|
||||
writeFileSync(out, chunks.join(''))
|
||||
}
|
||||
process.exit(0)
|
||||
} catch (err) {
|
||||
console.error(String(err))
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
ws.onerror = err => { console.error('ws error', err.message ?? err); process.exit(1) }
|
||||
17
bench/live-render.mjs
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
// live-render.mjs — quick chart from live-attach samples: node bench/live-render.mjs <dir>
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
const dir = process.argv[2] ?? '.'
|
||||
const rows = readFileSync(`${dir}/samples.jsonl`, 'utf8').trim().split('\n').map(l => JSON.parse(l))
|
||||
const t0 = rows[0].t
|
||||
const pts = rows.map(r => ({ t: r.t - t0, rss: r.rss_kb / 1024, hwm: r.vmhwm_kb / 1024 }))
|
||||
const W = 900, H = 360, mt = (v, max) => H - 30 - (v / max) * (H - 60)
|
||||
const maxY = Math.max(...pts.map(p => p.hwm)) * 1.1
|
||||
const path = k => pts.map((p, i) => `${i ? 'L' : 'M'}${30 + (p.t / pts.at(-1).t) * (W - 60)},${mt(p[k], maxY)}`).join('')
|
||||
const cpu = rows.map((r, i) => i ? (r.cpu_ticks - rows[i-1].cpu_ticks) / r.cpu_hz / (r.t - rows[i-1].t) : 0)
|
||||
writeFileSync(`${dir}/live.svg`, `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" style="background:#0d0d12">
|
||||
<text x="30" y="20" fill="#ccc" font-family="monospace">live session: RSS (gold) / VmHWM (grey) MB · avg cpu ${(cpu.reduce((a,b)=>a+b,0)/Math.max(1,cpu.length-1)*100).toFixed(1)}% · ${rows.length}s</text>
|
||||
<path d="${path('hwm')}" stroke="#888" fill="none"/><path d="${path('rss')}" stroke="#F5B820" fill="none" stroke-width="2"/>
|
||||
<text x="30" y="${H-10}" fill="#888" font-family="monospace">0s</text><text x="${W-80}" y="${H-10}" fill="#888" font-family="monospace">${Math.round(pts.at(-1).t)}s</text>
|
||||
<text x="${W-120}" y="${mt(pts.at(-1).rss,maxY)}" fill="#F5B820" font-family="monospace">${pts.at(-1).rss.toFixed(0)}MB</text></svg>`)
|
||||
console.log(`${dir}/live.svg`)
|
||||
31
bench/package-lock.json
generated
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@hermes/bench",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@hermes/bench",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"node-pty": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
|
||||
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
bench/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@hermes/bench",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "TUI benchmark suite: Ink (ui-tui) vs OpenTUI (ui-opentui) over a real PTY with a fake gateway. Methodology: docs/plans/opentui-bench-suite.md.",
|
||||
"scripts": {
|
||||
"check": "node --check fake-gateway.mjs && node --check fixture-stream.mjs && node --check harness.mjs && node --check run.mjs && node --check render.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-pty": "^1.1.0"
|
||||
}
|
||||
}
|
||||
1102
bench/render.mjs
Normal file
BIN
bench/report-assets/frame-gaps.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
bench/report-assets/frame-rate.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
bench/report-assets/mem-real-workloads.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
bench/report-assets/node-count.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
bench/report-assets/pipeline-cpu.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
bench/report-assets/pty-rate.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
bench/report-assets/rss-vs-msgs.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
bench/report-assets/scroll-cdf.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
bench/report-assets/session-histogram.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
bench/report-assets/startup.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
584
bench/report.html
Normal file
393
bench/results/2026-06-10T2043-50e3471-gate-ink-ink-r0.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "gate",
|
||||
"ui": "ink",
|
||||
"config": "ink",
|
||||
"mode": "digest",
|
||||
"rep": 0,
|
||||
"run_id": "mq8jcwon-vztv",
|
||||
"utc": "2026-06-10T20:43:15.191Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": null,
|
||||
"opentui_cap": null,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-300.ndjson",
|
||||
"msgs": 300,
|
||||
"sha256": "ac81e975c299da7d50cfda7fc0fee33c356bd31a4e17a7f0f8e49905e205051a"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2350535,
|
||||
"gw_pid": 2350547,
|
||||
"cgroup": null,
|
||||
"load_avg_at_start": [
|
||||
0.09,
|
||||
0.18,
|
||||
0.29
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 28,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 54864,
|
||||
"pss_kb": 28606,
|
||||
"private_dirty_kb": 17440,
|
||||
"vmhwm_kb": 54904,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 1
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1035,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 6615,
|
||||
"pty_writes": 11,
|
||||
"rss_kb": 109772,
|
||||
"pss_kb": 70491,
|
||||
"private_dirty_kb": 50616,
|
||||
"vmhwm_kb": 109772,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 3
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1461,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 14431,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 127628,
|
||||
"pss_kb": 88252,
|
||||
"private_dirty_kb": 67748,
|
||||
"vmhwm_kb": 127648,
|
||||
"utime_ticks": 37,
|
||||
"stime_ticks": 4
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1512,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 21088,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 162132,
|
||||
"pss_kb": 122735,
|
||||
"private_dirty_kb": 102252,
|
||||
"vmhwm_kb": 162148,
|
||||
"utime_ticks": 45,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1538,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 21088,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 172044,
|
||||
"pss_kb": 132615,
|
||||
"private_dirty_kb": 112100,
|
||||
"vmhwm_kb": 172288,
|
||||
"utime_ticks": 50,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 1541,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 21088,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 172640,
|
||||
"pss_kb": 133211,
|
||||
"private_dirty_kb": 112696,
|
||||
"vmhwm_kb": 173288,
|
||||
"utime_ticks": 50,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2039,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 28559,
|
||||
"pty_writes": 23,
|
||||
"rss_kb": 179128,
|
||||
"pss_kb": 139667,
|
||||
"private_dirty_kb": 119184,
|
||||
"vmhwm_kb": 184828,
|
||||
"utime_ticks": 57,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "final",
|
||||
"t_ms": 2919,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 28604,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 179136,
|
||||
"pss_kb": 139675,
|
||||
"private_dirty_kb": 119192,
|
||||
"vmhwm_kb": 184828,
|
||||
"utime_ticks": 57,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3046,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 32118,
|
||||
"pty_writes": 26,
|
||||
"rss_kb": 185304,
|
||||
"pss_kb": 145843,
|
||||
"private_dirty_kb": 125360,
|
||||
"vmhwm_kb": 185304,
|
||||
"utime_ticks": 61,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4050,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 42828,
|
||||
"pty_writes": 32,
|
||||
"rss_kb": 186164,
|
||||
"pss_kb": 146703,
|
||||
"private_dirty_kb": 126220,
|
||||
"vmhwm_kb": 186164,
|
||||
"utime_ticks": 63,
|
||||
"stime_ticks": 6
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 177
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "commands.catalog",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "setup.status",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.active_list",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1410
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1435
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1435
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1461
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1461
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1461
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1485
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1512
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1512
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1541
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1560
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1560
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1585
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1585
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1585
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.active_list",
|
||||
"t_ms": 1710
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 2944
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 2944
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "terminal.resize",
|
||||
"t_ms": 3044
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 3094
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.active_list",
|
||||
"t_ms": 3220
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 3322
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 3346
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "terminal.resize",
|
||||
"t_ms": 3422
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 3497
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "completed",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 0,
|
||||
"signal": 0,
|
||||
"t": 4530
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 300,
|
||||
"events_streamed": 1051,
|
||||
"pty_bytes_total": 43054,
|
||||
"pty_data_callbacks": 44,
|
||||
"first_byte_ms": 71,
|
||||
"session_create_ms": 202,
|
||||
"stream_start_ms": 1410,
|
||||
"vmhwm_kb": 186164,
|
||||
"cg_peak": null,
|
||||
"drain_max_loop_lag_ms": 6,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": "7775bee02e57da2be0f73880cfe828666f51e08209a28ba81ec599817b95eb2c"
|
||||
},
|
||||
"digest_text": "Excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit.│Amet culpa cillum commodo enim adipiscing deserunt nulla duis veniam sed.│Quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna.│Esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute quis.│Anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip.│Magna amet culpa cillum.│Aute quis eiusmod lorem occaecat.│ └─ ● Terminal(\"Proident velit laboris magna.\") (0.3s)│ └─ Args:│Proident velit laboris magna amet culpa cillum commodo enim adipiscing deserunt nulla.│Elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident.│Ullamco labore sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit.│Nulla duis veniam sed.│Dolor proident velit laboris magna.│Result:│Pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris.│Sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute.││┊ Minimelitmollitpariaturautequiseiusmodloremoccaecatreprehenderitexercitationincididuntdolor.Officia│fugiatconsequatminimelitmollitpariaturautequiseiusmodloremoccaecatreprehenderitexercitation.│┊ Temporipsumcupidatatvoluptateullamcolaboresitsuntessealiquipaliqua.Excepteurirurenostrudtempor│ipsumcupidatatvoluptateullamcolaboresitsuntesse.Veniamsedanimexcepteurirurenostrudtempor│ipsumcupidatatvoluptateullamcolaboresit.││• Suntessealiquipaliquaconsectetur.│• Consequatminimelitmollitpariaturautequis.│• Eiusmodloremoccaecatreprehenderit.││─ ts│constx4=58│functionf3(){│returnx│}││Exercitationincididuntdolorproidentvelitlaborismagnaametculpacillum.Loremoccaecatreprehenderit┃exercitationincididuntdolorproidentvelitlaborismagnaamet.─ ready │ fake model │ 3s │ voice off ─ …es-agent (…i-native-engine)❯│┃Excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit.│Amet culpa cillum commodo enim adipiscing deserunt nulla duis veniam sed.│Quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna.│Esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute quis.│Anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip.│Magna amet culpa cillum.│Aute quis eiusmod lorem occaecat.│ └─ ● Terminal(\"Proident velit laboris magna.\") (0.3s)│ └─ Args:│Proident velit laboris magna amet culpa cillum commodo enim adipiscing deserunt nulla.│Elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident.│Ullamco labore sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit.│Nulla duis veniam sed.│Dolor proident velit laboris magna.│Result:│Pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris.│Sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute.││┊ Minimelitmollitpariaturautequiseiusmodloremoccaecatreprehenderitexercitationincididuntdolor.Officia│fugiatconsequatminimelitmollitpariaturautequiseiusmodloremoccaecatreprehenderitexercitation.│┊ Temporipsumcupidatatvoluptateullamcolaboresitsuntessealiquipaliqua.Excepteurirurenostrudtempor│ipsumcupidatatvoluptateullamcolaboresitsuntesse.Veniamsedanimexcepteurirurenostrudtempor│ipsumcupidatatvoluptateullamcolaboresit.││• Suntessealiquipaliquaconsectetur.│• Consequatminimelitmollitpariaturautequis.│• Eiusmodloremoccaecatreprehenderit.││─ ts│constx4=58│functionf3(){│returnx│}││Exercitationincididuntdolorproidentvelitlaborismagnaametculpacillum.Loremoccaecatreprehenderit│exercitationincididuntdolorproidentvelitlaborismagnaamet.┃─ ready │ fake model │ 3s │ voice off ─ …es-agent (…i-native-engine)❯4"
|
||||
}
|
||||
408
bench/results/2026-06-10T2043-50e3471-gate-ink-ink-r1.json
Normal file
@@ -0,0 +1,408 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "gate",
|
||||
"ui": "ink",
|
||||
"config": "ink",
|
||||
"mode": "digest",
|
||||
"rep": 1,
|
||||
"run_id": "mq8jd80l-wl8i",
|
||||
"utc": "2026-06-10T20:43:29.877Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": null,
|
||||
"opentui_cap": null,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-300.ndjson",
|
||||
"msgs": 300,
|
||||
"sha256": "ac81e975c299da7d50cfda7fc0fee33c356bd31a4e17a7f0f8e49905e205051a"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2350865,
|
||||
"gw_pid": 2350874,
|
||||
"cgroup": null,
|
||||
"load_avg_at_start": [
|
||||
0.07,
|
||||
0.17,
|
||||
0.29
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 26,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 56380,
|
||||
"pss_kb": 30130,
|
||||
"private_dirty_kb": 18964,
|
||||
"vmhwm_kb": 56432,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 1
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1034,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 6615,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 108556,
|
||||
"pss_kb": 69243,
|
||||
"private_dirty_kb": 49468,
|
||||
"vmhwm_kb": 108556,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1442,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 10643,
|
||||
"pty_writes": 15,
|
||||
"rss_kb": 124760,
|
||||
"pss_kb": 85364,
|
||||
"private_dirty_kb": 64888,
|
||||
"vmhwm_kb": 124768,
|
||||
"utime_ticks": 32,
|
||||
"stime_ticks": 3
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1493,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 18617,
|
||||
"pty_writes": 18,
|
||||
"rss_kb": 158216,
|
||||
"pss_kb": 118777,
|
||||
"private_dirty_kb": 98344,
|
||||
"vmhwm_kb": 158248,
|
||||
"utime_ticks": 41,
|
||||
"stime_ticks": 4
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1544,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 22520,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 192596,
|
||||
"pss_kb": 153125,
|
||||
"private_dirty_kb": 132660,
|
||||
"vmhwm_kb": 192732,
|
||||
"utime_ticks": 50,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 1546,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 22520,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 192964,
|
||||
"pss_kb": 153493,
|
||||
"private_dirty_kb": 133028,
|
||||
"vmhwm_kb": 193460,
|
||||
"utime_ticks": 50,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2049,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 34018,
|
||||
"pty_writes": 23,
|
||||
"rss_kb": 186672,
|
||||
"pss_kb": 147199,
|
||||
"private_dirty_kb": 126736,
|
||||
"vmhwm_kb": 195184,
|
||||
"utime_ticks": 60,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "final",
|
||||
"t_ms": 2909,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 34063,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 186680,
|
||||
"pss_kb": 147209,
|
||||
"private_dirty_kb": 126744,
|
||||
"vmhwm_kb": 195184,
|
||||
"utime_ticks": 60,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3057,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 37577,
|
||||
"pty_writes": 26,
|
||||
"rss_kb": 187024,
|
||||
"pss_kb": 147553,
|
||||
"private_dirty_kb": 127088,
|
||||
"vmhwm_kb": 195184,
|
||||
"utime_ticks": 63,
|
||||
"stime_ticks": 5
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4072,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 48287,
|
||||
"pty_writes": 32,
|
||||
"rss_kb": 187088,
|
||||
"pss_kb": 147617,
|
||||
"private_dirty_kb": 127152,
|
||||
"vmhwm_kb": 195184,
|
||||
"utime_ticks": 65,
|
||||
"stime_ticks": 6
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 176
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "commands.catalog",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "setup.status",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.active_list",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1416
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1416
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1442
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1442
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1466
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1492
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1517
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1517
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1517
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1546
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1567
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1567
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1592
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1592
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1616
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1616
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 1616
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.active_list",
|
||||
"t_ms": 1717
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 2930
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 2930
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "terminal.resize",
|
||||
"t_ms": 3030
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 3081
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.active_list",
|
||||
"t_ms": 3207
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 3332
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 3332
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "terminal.resize",
|
||||
"t_ms": 3410
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 3488
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "completed",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 0,
|
||||
"signal": 0,
|
||||
"t": 4520
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 300,
|
||||
"events_streamed": 1051,
|
||||
"pty_bytes_total": 48513,
|
||||
"pty_data_callbacks": 42,
|
||||
"first_byte_ms": 65,
|
||||
"session_create_ms": 201,
|
||||
"stream_start_ms": 1416,
|
||||
"vmhwm_kb": 195184,
|
||||
"cg_peak": null,
|
||||
"drain_max_loop_lag_ms": 2,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": "7775bee02e57da2be0f73880cfe828666f51e08209a28ba81ec599817b95eb2c"
|
||||
},
|
||||
"digest_text": "Excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit.│Amet culpa cillum commodo enim adipiscing deserunt nulla duis veniam sed.│Quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna.│Esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute quis.│Anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip.│Magna amet culpa cillum.│Aute quis eiusmod lorem occaecat.│ └─ ● Terminal(\"Proident velit laboris magna.\") (0.3s)│ └─ Args:│Proident velit laboris magna amet culpa cillum commodo enim adipiscing deserunt nulla.│Elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident.│Ullamco labore sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit.│Nulla duis veniam sed.│Dolor proident velit laboris magna.│Result:│Pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris.│Sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute.││┊ Minimelitmollitpariaturautequiseiusmodloremoccaecatreprehenderitexercitationincididuntdolor.Officia│fugiatconsequatminimelitmollitpariaturautequiseiusmodloremoccaecatreprehenderitexercitation.│┊ Temporipsumcupidatatvoluptateullamcolaboresitsuntessealiquipaliqua.Excepteurirurenostrudtempor│ipsumcupidatatvoluptateullamcolaboresitsuntesse.Veniamsedanimexcepteurirurenostrudtempor│ipsumcupidatatvoluptateullamcolaboresit.││• Suntessealiquipaliquaconsectetur.│• Consequatminimelitmollitpariaturautequis.│• Eiusmodloremoccaecatreprehenderit.││─ ts│constx4=58│functionf3(){│returnx│}││Exercitationincididuntdolorproidentvelitlaborismagnaametculpacillum.Loremoccaecatreprehenderit┃exercitationincididuntdolorproidentvelitlaborismagnaamet.─ ready │ fake model │ 3s │ voice off ─ …es-agent (…i-native-engine)❯│┃Excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit.│Amet culpa cillum commodo enim adipiscing deserunt nulla duis veniam sed.│Quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna.│Esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute quis.│Anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip.│Magna amet culpa cillum.│Aute quis eiusmod lorem occaecat.│ └─ ● Terminal(\"Proident velit laboris magna.\") (0.3s)│ └─ Args:│Proident velit laboris magna amet culpa cillum commodo enim adipiscing deserunt nulla.│Elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident.│Ullamco labore sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit.│Nulla duis veniam sed.│Dolor proident velit laboris magna.│Result:│Pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris.│Sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute.││┊ Minimelitmollitpariaturautequiseiusmodloremoccaecatreprehenderitexercitationincididuntdolor.Officia│fugiatconsequatminimelitmollitpariaturautequiseiusmodloremoccaecatreprehenderitexercitation.│┊ Temporipsumcupidatatvoluptateullamcolaboresitsuntessealiquipaliqua.Excepteurirurenostrudtempor│ipsumcupidatatvoluptateullamcolaboresitsuntesse.Veniamsedanimexcepteurirurenostrudtempor│ipsumcupidatatvoluptateullamcolaboresit.││• Suntessealiquipaliquaconsectetur.│• Consequatminimelitmollitpariaturautequis.│• Eiusmodloremoccaecatreprehenderit.││─ ts│constx4=58│functionf3(){│returnx│}││Exercitationincididuntdolorproidentvelitlaborismagnaametculpacillum.Loremoccaecatreprehenderit│exercitationincididuntdolorproidentvelitlaborismagnaamet.┃─ ready │ fake model │ 3s │ voice off ─ …es-agent (…i-native-engine)❯4"
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "gate",
|
||||
"ui": "opentui",
|
||||
"config": "otui-capped",
|
||||
"mode": "digest",
|
||||
"rep": 0,
|
||||
"run_id": "mq8jdjbz-h2u1",
|
||||
"utc": "2026-06-10T20:43:44.543Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": null,
|
||||
"opentui_cap": 3000,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-300.ndjson",
|
||||
"msgs": 300,
|
||||
"sha256": "ac81e975c299da7d50cfda7fc0fee33c356bd31a4e17a7f0f8e49905e205051a"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2351291,
|
||||
"gw_pid": 2351309,
|
||||
"cgroup": null,
|
||||
"load_avg_at_start": [
|
||||
0.05,
|
||||
0.17,
|
||||
0.28
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 25,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 54500,
|
||||
"pss_kb": 28086,
|
||||
"private_dirty_kb": 16976,
|
||||
"vmhwm_kb": 54544,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1032,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 105296,
|
||||
"pss_kb": 65020,
|
||||
"private_dirty_kb": 48488,
|
||||
"vmhwm_kb": 107988,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1383,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 112456,
|
||||
"pss_kb": 72079,
|
||||
"private_dirty_kb": 55520,
|
||||
"vmhwm_kb": 112468,
|
||||
"utime_ticks": 22,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1384,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 112480,
|
||||
"pss_kb": 72103,
|
||||
"private_dirty_kb": 55544,
|
||||
"vmhwm_kb": 112480,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1384,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 112488,
|
||||
"pss_kb": 72111,
|
||||
"private_dirty_kb": 55552,
|
||||
"vmhwm_kb": 112508,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 1385,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 112612,
|
||||
"pss_kb": 72235,
|
||||
"private_dirty_kb": 55676,
|
||||
"vmhwm_kb": 112640,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 3
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2035,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 35562,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 274188,
|
||||
"pss_kb": 231176,
|
||||
"private_dirty_kb": 213680,
|
||||
"vmhwm_kb": 301852,
|
||||
"utime_ticks": 106,
|
||||
"stime_ticks": 11
|
||||
},
|
||||
{
|
||||
"kind": "final",
|
||||
"t_ms": 2863,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 35634,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 274220,
|
||||
"pss_kb": 231208,
|
||||
"private_dirty_kb": 213712,
|
||||
"vmhwm_kb": 301852,
|
||||
"utime_ticks": 107,
|
||||
"stime_ticks": 11
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3045,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 46545,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 274276,
|
||||
"pss_kb": 231264,
|
||||
"private_dirty_kb": 213768,
|
||||
"vmhwm_kb": 301852,
|
||||
"utime_ticks": 108,
|
||||
"stime_ticks": 11
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4051,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 57867,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 274312,
|
||||
"pss_kb": 231300,
|
||||
"private_dirty_kb": 213804,
|
||||
"vmhwm_kb": 301852,
|
||||
"utime_ticks": 109,
|
||||
"stime_ticks": 11
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 175
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 175
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 175
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "completed",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 0,
|
||||
"signal": 0,
|
||||
"t": 4725
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 300,
|
||||
"events_streamed": 1051,
|
||||
"pty_bytes_total": 62678,
|
||||
"pty_data_callbacks": 35,
|
||||
"first_byte_ms": 128,
|
||||
"session_create_ms": 175,
|
||||
"stream_start_ms": 1382,
|
||||
"vmhwm_kb": 301852,
|
||||
"cg_peak": null,
|
||||
"drain_max_loop_lag_ms": 3,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": "d5e9558583159eac9e72e450848f98505ac9f48804d4f8c80a18baaab0f0f28c"
|
||||
},
|
||||
"digest_text": "⚕ Hermes Agent · opentui · ready ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── $ terminal Culpa cillum commodo enim. · 3.7s (2 lines) ◇ read_file Duis veniam sed anim. · 3.8s (18 lines) ◦ edit_file Tempor ipsum cupidatat voluptate. · 3.9s (7 lines) ◦ grep Sunt esse aliquip aliqua. · 4.0s (2 lines) ● web_search Consequat minim elit mollit. · 0.1s (18 lines) ◆ write_file Eiusmod lorem occaecat reprehenderit. · 0.2s (7 lines) $ terminal Proident velit laboris magna. · 0.3s (2 lines) Minim elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor. Officia fugiat consequat minim elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation. ⧉ copy ⚕ Tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip aliqua. Excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse. Veniam sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit. - Sunt esse aliquip aliqua consectetur. - Consequat minim elit mollit pariatur aute quis. - Eiusmod lorem occaecat reprehenderit. const x4 = 58 function f3() { return x } Exercitation incididunt dolor proident velit laboris magna amet culpa cillum. Lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna amet. ⧉ copy ▄ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ● fake-model │ up: Ns │ …/lively-thrush/hermes-agent ❯ Type your message"
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "gate",
|
||||
"ui": "opentui",
|
||||
"config": "otui-capped",
|
||||
"mode": "digest",
|
||||
"rep": 1,
|
||||
"run_id": "mq8jdupc-im2a",
|
||||
"utc": "2026-06-10T20:43:59.280Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": null,
|
||||
"opentui_cap": 3000,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-300.ndjson",
|
||||
"msgs": 300,
|
||||
"sha256": "ac81e975c299da7d50cfda7fc0fee33c356bd31a4e17a7f0f8e49905e205051a"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2351822,
|
||||
"gw_pid": 2351831,
|
||||
"cgroup": null,
|
||||
"load_avg_at_start": [
|
||||
0.04,
|
||||
0.16,
|
||||
0.28
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 27,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 52404,
|
||||
"pss_kb": 26288,
|
||||
"private_dirty_kb": 15184,
|
||||
"vmhwm_kb": 52628,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1033,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 10,
|
||||
"rss_kb": 104956,
|
||||
"pss_kb": 64728,
|
||||
"private_dirty_kb": 48196,
|
||||
"vmhwm_kb": 106820,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1385,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 112924,
|
||||
"pss_kb": 72595,
|
||||
"private_dirty_kb": 56036,
|
||||
"vmhwm_kb": 112968,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1386,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 112968,
|
||||
"pss_kb": 72639,
|
||||
"private_dirty_kb": 56080,
|
||||
"vmhwm_kb": 112968,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1386,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 112972,
|
||||
"pss_kb": 72643,
|
||||
"private_dirty_kb": 56084,
|
||||
"vmhwm_kb": 112972,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 1387,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 112984,
|
||||
"pss_kb": 72655,
|
||||
"private_dirty_kb": 56096,
|
||||
"vmhwm_kb": 113052,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2046,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 35562,
|
||||
"pty_writes": 15,
|
||||
"rss_kb": 283176,
|
||||
"pss_kb": 240212,
|
||||
"private_dirty_kb": 222716,
|
||||
"vmhwm_kb": 303016,
|
||||
"utime_ticks": 105,
|
||||
"stime_ticks": 10
|
||||
},
|
||||
{
|
||||
"kind": "final",
|
||||
"t_ms": 2862,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 35634,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 283208,
|
||||
"pss_kb": 240244,
|
||||
"private_dirty_kb": 222748,
|
||||
"vmhwm_kb": 303016,
|
||||
"utime_ticks": 105,
|
||||
"stime_ticks": 10
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3060,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 46545,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 283264,
|
||||
"pss_kb": 240300,
|
||||
"private_dirty_kb": 222804,
|
||||
"vmhwm_kb": 303016,
|
||||
"utime_ticks": 107,
|
||||
"stime_ticks": 10
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4074,
|
||||
"msgs": 300,
|
||||
"events": null,
|
||||
"pty_bytes": 57867,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 283308,
|
||||
"pss_kb": 240344,
|
||||
"private_dirty_kb": 222848,
|
||||
"vmhwm_kb": 303016,
|
||||
"utime_ticks": 107,
|
||||
"stime_ticks": 10
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 176
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 176
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 176
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "completed",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 0,
|
||||
"signal": 0,
|
||||
"t": 4706
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 300,
|
||||
"events_streamed": 1051,
|
||||
"pty_bytes_total": 62678,
|
||||
"pty_data_callbacks": 30,
|
||||
"first_byte_ms": 130,
|
||||
"session_create_ms": 176,
|
||||
"stream_start_ms": 1383,
|
||||
"vmhwm_kb": 303016,
|
||||
"cg_peak": null,
|
||||
"drain_max_loop_lag_ms": 4,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": "d5e9558583159eac9e72e450848f98505ac9f48804d4f8c80a18baaab0f0f28c"
|
||||
},
|
||||
"digest_text": "⚕ Hermes Agent · opentui · ready ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── $ terminal Culpa cillum commodo enim. · 3.7s (2 lines) ◇ read_file Duis veniam sed anim. · 3.8s (18 lines) ◦ edit_file Tempor ipsum cupidatat voluptate. · 3.9s (7 lines) ◦ grep Sunt esse aliquip aliqua. · 4.0s (2 lines) ● web_search Consequat minim elit mollit. · 0.1s (18 lines) ◆ write_file Eiusmod lorem occaecat reprehenderit. · 0.2s (7 lines) $ terminal Proident velit laboris magna. · 0.3s (2 lines) Minim elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor. Officia fugiat consequat minim elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation. ⧉ copy ⚕ Tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip aliqua. Excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse. Veniam sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit. - Sunt esse aliquip aliqua consectetur. - Consequat minim elit mollit pariatur aute quis. - Eiusmod lorem occaecat reprehenderit. const x4 = 58 function f3() { return x } Exercitation incididunt dolor proident velit laboris magna amet culpa cillum. Lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna amet. ⧉ copy ▄ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ● fake-model │ up: Ns │ …/lively-thrush/hermes-agent ❯ Type your message"
|
||||
}
|
||||
@@ -0,0 +1,799 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "mem3000",
|
||||
"ui": "opentui",
|
||||
"config": "otui-capped",
|
||||
"mode": "mem",
|
||||
"rep": 0,
|
||||
"run_id": "mq8jwe2j-9ikb",
|
||||
"utc": "2026-06-10T20:58:24.187Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": "2G",
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": 3000,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-3000.ndjson",
|
||||
"msgs": 3000,
|
||||
"sha256": "0df05a04a611dda68aa07865f21c45b08edc78e0a71d4c8cb2b674729778d96d"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2367235,
|
||||
"gw_pid": 2367245,
|
||||
"cgroup": "/sys/fs/cgroup/user.slice/user-1001.slice/user@1001.service/app.slice/hermes-bench-mq8jwe2j-9ikb.scope",
|
||||
"load_avg_at_start": [
|
||||
0.25,
|
||||
0.35,
|
||||
0.39
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 53,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 60864,
|
||||
"pss_kb": 34205,
|
||||
"private_dirty_kb": 22956,
|
||||
"vmhwm_kb": 60992,
|
||||
"utime_ticks": 3,
|
||||
"stime_ticks": 0,
|
||||
"cg_current": 25100288,
|
||||
"cg_peak": 25100288,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1062,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 10,
|
||||
"rss_kb": 104924,
|
||||
"pss_kb": 64715,
|
||||
"private_dirty_kb": 48120,
|
||||
"vmhwm_kb": 107988,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 61902848,
|
||||
"cg_peak": 66801664,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1695,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 106908,
|
||||
"pss_kb": 66676,
|
||||
"private_dirty_kb": 50104,
|
||||
"vmhwm_kb": 107988,
|
||||
"utime_ticks": 18,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 72585216,
|
||||
"cg_peak": 72609792,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1719,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 116132,
|
||||
"pss_kb": 75807,
|
||||
"private_dirty_kb": 59200,
|
||||
"vmhwm_kb": 116156,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 83578880,
|
||||
"cg_peak": 83726336,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1720,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 116200,
|
||||
"pss_kb": 75875,
|
||||
"private_dirty_kb": 59268,
|
||||
"vmhwm_kb": 116352,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 83578880,
|
||||
"cg_peak": 83726336,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1721,
|
||||
"msgs": 400,
|
||||
"events": 1432,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 116356,
|
||||
"pss_kb": 75945,
|
||||
"private_dirty_kb": 59296,
|
||||
"vmhwm_kb": 116356,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 83578880,
|
||||
"cg_peak": 83726336,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1721,
|
||||
"msgs": 501,
|
||||
"events": 1792,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 116356,
|
||||
"pss_kb": 75945,
|
||||
"private_dirty_kb": 59296,
|
||||
"vmhwm_kb": 116364,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 83345408,
|
||||
"cg_peak": 83726336,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2073,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 244776,
|
||||
"pss_kb": 202114,
|
||||
"private_dirty_kb": 184712,
|
||||
"vmhwm_kb": 244784,
|
||||
"utime_ticks": 81,
|
||||
"stime_ticks": 7,
|
||||
"cg_current": 214659072,
|
||||
"cg_peak": 214659072,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2222,
|
||||
"msgs": 601,
|
||||
"events": 2154,
|
||||
"pty_bytes": 38613,
|
||||
"pty_writes": 15,
|
||||
"rss_kb": 251684,
|
||||
"pss_kb": 208873,
|
||||
"private_dirty_kb": 191432,
|
||||
"vmhwm_kb": 251712,
|
||||
"utime_ticks": 100,
|
||||
"stime_ticks": 8,
|
||||
"cg_current": 223088640,
|
||||
"cg_peak": 223354880,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2449,
|
||||
"msgs": 701,
|
||||
"events": 2496,
|
||||
"pty_bytes": 42490,
|
||||
"pty_writes": 17,
|
||||
"rss_kb": 319152,
|
||||
"pss_kb": 276164,
|
||||
"private_dirty_kb": 258648,
|
||||
"vmhwm_kb": 320132,
|
||||
"utime_ticks": 137,
|
||||
"stime_ticks": 11,
|
||||
"cg_current": 292880384,
|
||||
"cg_peak": 295047168,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2727,
|
||||
"msgs": 801,
|
||||
"events": 2857,
|
||||
"pty_bytes": 47176,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 337492,
|
||||
"pss_kb": 294504,
|
||||
"private_dirty_kb": 276988,
|
||||
"vmhwm_kb": 349112,
|
||||
"utime_ticks": 181,
|
||||
"stime_ticks": 14,
|
||||
"cg_current": 312528896,
|
||||
"cg_peak": 325537792,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3054,
|
||||
"msgs": 901,
|
||||
"events": 3245,
|
||||
"pty_bytes": 49632,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 370356,
|
||||
"pss_kb": 327363,
|
||||
"private_dirty_kb": 309852,
|
||||
"vmhwm_kb": 370368,
|
||||
"utime_ticks": 216,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 347090944,
|
||||
"cg_peak": 347090944,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3079,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 49632,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 377708,
|
||||
"pss_kb": 334715,
|
||||
"private_dirty_kb": 317204,
|
||||
"vmhwm_kb": 377752,
|
||||
"utime_ticks": 220,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 354160640,
|
||||
"cg_peak": 354422784,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3305,
|
||||
"msgs": 1001,
|
||||
"events": 3588,
|
||||
"pty_bytes": 50573,
|
||||
"pty_writes": 22,
|
||||
"rss_kb": 398476,
|
||||
"pss_kb": 355483,
|
||||
"private_dirty_kb": 337972,
|
||||
"vmhwm_kb": 398476,
|
||||
"utime_ticks": 243,
|
||||
"stime_ticks": 17,
|
||||
"cg_current": 375959552,
|
||||
"cg_peak": 375963648,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3683,
|
||||
"msgs": 1101,
|
||||
"events": 3928,
|
||||
"pty_bytes": 54942,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 436236,
|
||||
"pss_kb": 393243,
|
||||
"private_dirty_kb": 375732,
|
||||
"vmhwm_kb": 436236,
|
||||
"utime_ticks": 290,
|
||||
"stime_ticks": 18,
|
||||
"cg_current": 414687232,
|
||||
"cg_peak": 414982144,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3687,
|
||||
"msgs": 1201,
|
||||
"events": 4298,
|
||||
"pty_bytes": 54942,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 436236,
|
||||
"pss_kb": 393243,
|
||||
"private_dirty_kb": 375732,
|
||||
"vmhwm_kb": 436512,
|
||||
"utime_ticks": 291,
|
||||
"stime_ticks": 19,
|
||||
"cg_current": 415211520,
|
||||
"cg_peak": 415211520,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3692,
|
||||
"msgs": 1300,
|
||||
"events": 4659,
|
||||
"pty_bytes": 54942,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 436760,
|
||||
"pss_kb": 393767,
|
||||
"private_dirty_kb": 376256,
|
||||
"vmhwm_kb": 436772,
|
||||
"utime_ticks": 293,
|
||||
"stime_ticks": 19,
|
||||
"cg_current": 415473664,
|
||||
"cg_peak": 415473664,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3697,
|
||||
"msgs": 1400,
|
||||
"events": 5011,
|
||||
"pty_bytes": 54942,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 437124,
|
||||
"pss_kb": 394131,
|
||||
"private_dirty_kb": 376620,
|
||||
"vmhwm_kb": 437308,
|
||||
"utime_ticks": 295,
|
||||
"stime_ticks": 19,
|
||||
"cg_current": 415997952,
|
||||
"cg_peak": 415997952,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4086,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 54942,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 534280,
|
||||
"pss_kb": 491287,
|
||||
"private_dirty_kb": 473776,
|
||||
"vmhwm_kb": 534288,
|
||||
"utime_ticks": 348,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 517648384,
|
||||
"cg_peak": 517742592,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4236,
|
||||
"msgs": 1500,
|
||||
"events": 5384,
|
||||
"pty_bytes": 57789,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 539180,
|
||||
"pss_kb": 496187,
|
||||
"private_dirty_kb": 478676,
|
||||
"vmhwm_kb": 539344,
|
||||
"utime_ticks": 364,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 523218944,
|
||||
"cg_peak": 523218944,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4241,
|
||||
"msgs": 1600,
|
||||
"events": 5730,
|
||||
"pty_bytes": 57789,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 539508,
|
||||
"pss_kb": 496515,
|
||||
"private_dirty_kb": 479004,
|
||||
"vmhwm_kb": 539584,
|
||||
"utime_ticks": 364,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 523743232,
|
||||
"cg_peak": 523743232,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4244,
|
||||
"msgs": 1700,
|
||||
"events": 6100,
|
||||
"pty_bytes": 57789,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 539728,
|
||||
"pss_kb": 496735,
|
||||
"private_dirty_kb": 479224,
|
||||
"vmhwm_kb": 539900,
|
||||
"utime_ticks": 365,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 523743232,
|
||||
"cg_peak": 523743232,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4260,
|
||||
"msgs": 1800,
|
||||
"events": 6455,
|
||||
"pty_bytes": 57789,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 541568,
|
||||
"pss_kb": 498575,
|
||||
"private_dirty_kb": 481064,
|
||||
"vmhwm_kb": 541648,
|
||||
"utime_ticks": 367,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 525840384,
|
||||
"cg_peak": 525840384,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4265,
|
||||
"msgs": 1900,
|
||||
"events": 6838,
|
||||
"pty_bytes": 57789,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 542688,
|
||||
"pss_kb": 499695,
|
||||
"private_dirty_kb": 482184,
|
||||
"vmhwm_kb": 543040,
|
||||
"utime_ticks": 368,
|
||||
"stime_ticks": 24,
|
||||
"cg_current": 527626240,
|
||||
"cg_peak": 527626240,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4269,
|
||||
"msgs": 2000,
|
||||
"events": 7151,
|
||||
"pty_bytes": 57789,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 543204,
|
||||
"pss_kb": 500211,
|
||||
"private_dirty_kb": 482700,
|
||||
"vmhwm_kb": 543388,
|
||||
"utime_ticks": 368,
|
||||
"stime_ticks": 24,
|
||||
"cg_current": 528150528,
|
||||
"cg_peak": 528150528,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4965,
|
||||
"msgs": 2100,
|
||||
"events": 7532,
|
||||
"pty_bytes": 59760,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 676032,
|
||||
"pss_kb": 633039,
|
||||
"private_dirty_kb": 615528,
|
||||
"vmhwm_kb": 676100,
|
||||
"utime_ticks": 446,
|
||||
"stime_ticks": 28,
|
||||
"cg_current": 665362432,
|
||||
"cg_peak": 665362432,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 5094,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 59760,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 702136,
|
||||
"pss_kb": 659143,
|
||||
"private_dirty_kb": 641632,
|
||||
"vmhwm_kb": 702136,
|
||||
"utime_ticks": 460,
|
||||
"stime_ticks": 30,
|
||||
"cg_current": 693010432,
|
||||
"cg_peak": 693010432,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5395,
|
||||
"msgs": 2201,
|
||||
"events": 7892,
|
||||
"pty_bytes": 62265,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 705816,
|
||||
"pss_kb": 662823,
|
||||
"private_dirty_kb": 645312,
|
||||
"vmhwm_kb": 705816,
|
||||
"utime_ticks": 491,
|
||||
"stime_ticks": 30,
|
||||
"cg_current": 696999936,
|
||||
"cg_peak": 696999936,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 6099,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 65269,
|
||||
"pty_writes": 30,
|
||||
"rss_kb": 739096,
|
||||
"pss_kb": 696103,
|
||||
"private_dirty_kb": 678592,
|
||||
"vmhwm_kb": 739136,
|
||||
"utime_ticks": 564,
|
||||
"stime_ticks": 32,
|
||||
"cg_current": 731422720,
|
||||
"cg_peak": 731422720,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 6126,
|
||||
"msgs": 2301,
|
||||
"events": 8254,
|
||||
"pty_bytes": 67277,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 739464,
|
||||
"pss_kb": 696471,
|
||||
"private_dirty_kb": 678960,
|
||||
"vmhwm_kb": 739464,
|
||||
"utime_ticks": 567,
|
||||
"stime_ticks": 32,
|
||||
"cg_current": 731725824,
|
||||
"cg_peak": 731856896,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 6880,
|
||||
"msgs": 2401,
|
||||
"events": 8596,
|
||||
"pty_bytes": 70614,
|
||||
"pty_writes": 33,
|
||||
"rss_kb": 771508,
|
||||
"pss_kb": 728515,
|
||||
"private_dirty_kb": 711004,
|
||||
"vmhwm_kb": 771508,
|
||||
"utime_ticks": 645,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 765329408,
|
||||
"cg_peak": 765329408,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 7109,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 72844,
|
||||
"pty_writes": 34,
|
||||
"rss_kb": 773340,
|
||||
"pss_kb": 730347,
|
||||
"private_dirty_kb": 712836,
|
||||
"vmhwm_kb": 773496,
|
||||
"utime_ticks": 668,
|
||||
"stime_ticks": 36,
|
||||
"cg_current": 767463424,
|
||||
"cg_peak": 767463424,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7658,
|
||||
"msgs": 2501,
|
||||
"events": 8957,
|
||||
"pty_bytes": 74604,
|
||||
"pty_writes": 35,
|
||||
"rss_kb": 802688,
|
||||
"pss_kb": 759695,
|
||||
"private_dirty_kb": 742184,
|
||||
"vmhwm_kb": 802688,
|
||||
"utime_ticks": 723,
|
||||
"stime_ticks": 38,
|
||||
"cg_current": 797646848,
|
||||
"cg_peak": 797773824,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 8113,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 76216,
|
||||
"pty_writes": 36,
|
||||
"rss_kb": 831540,
|
||||
"pss_kb": 788547,
|
||||
"private_dirty_kb": 771036,
|
||||
"vmhwm_kb": 831680,
|
||||
"utime_ticks": 769,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 828358656,
|
||||
"cg_peak": 828514304,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 8512,
|
||||
"msgs": 2601,
|
||||
"events": 9345,
|
||||
"pty_bytes": 77698,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 834300,
|
||||
"pss_kb": 791307,
|
||||
"private_dirty_kb": 773796,
|
||||
"vmhwm_kb": 834300,
|
||||
"utime_ticks": 810,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 830693376,
|
||||
"cg_peak": 831066112,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 8794,
|
||||
"msgs": 2701,
|
||||
"events": 9688,
|
||||
"pty_bytes": 79263,
|
||||
"pty_writes": 38,
|
||||
"rss_kb": 838572,
|
||||
"pss_kb": 795579,
|
||||
"private_dirty_kb": 778068,
|
||||
"vmhwm_kb": 838584,
|
||||
"utime_ticks": 839,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 835891200,
|
||||
"cg_peak": 835891200,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 9118,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 79263,
|
||||
"pty_writes": 38,
|
||||
"rss_kb": 862420,
|
||||
"pss_kb": 819427,
|
||||
"private_dirty_kb": 801916,
|
||||
"vmhwm_kb": 862420,
|
||||
"utime_ticks": 871,
|
||||
"stime_ticks": 42,
|
||||
"cg_current": 860319744,
|
||||
"cg_peak": 860368896,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9622,
|
||||
"msgs": 2801,
|
||||
"events": 10028,
|
||||
"pty_bytes": 83086,
|
||||
"pty_writes": 40,
|
||||
"rss_kb": 865588,
|
||||
"pss_kb": 822595,
|
||||
"private_dirty_kb": 805084,
|
||||
"vmhwm_kb": 865616,
|
||||
"utime_ticks": 922,
|
||||
"stime_ticks": 42,
|
||||
"cg_current": 863592448,
|
||||
"cg_peak": 863592448,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 10123,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 83086,
|
||||
"pty_writes": 40,
|
||||
"rss_kb": 895276,
|
||||
"pss_kb": 852283,
|
||||
"private_dirty_kb": 834772,
|
||||
"vmhwm_kb": 895276,
|
||||
"utime_ticks": 972,
|
||||
"stime_ticks": 44,
|
||||
"cg_current": 894144512,
|
||||
"cg_peak": 894468096,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 10275,
|
||||
"msgs": 2901,
|
||||
"events": 10398,
|
||||
"pty_bytes": 85844,
|
||||
"pty_writes": 42,
|
||||
"rss_kb": 898960,
|
||||
"pss_kb": 855967,
|
||||
"private_dirty_kb": 838456,
|
||||
"vmhwm_kb": 899064,
|
||||
"utime_ticks": 988,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 898830336,
|
||||
"cg_peak": 898830336,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 10281,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 85844,
|
||||
"pty_writes": 42,
|
||||
"rss_kb": 899388,
|
||||
"pss_kb": 856395,
|
||||
"private_dirty_kb": 838884,
|
||||
"vmhwm_kb": 899412,
|
||||
"utime_ticks": 988,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 899354624,
|
||||
"cg_peak": 899354624,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 10287,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 85844,
|
||||
"pty_writes": 42,
|
||||
"rss_kb": 899652,
|
||||
"pss_kb": 856659,
|
||||
"private_dirty_kb": 839148,
|
||||
"vmhwm_kb": 899988,
|
||||
"utime_ticks": 988,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 899878912,
|
||||
"cg_peak": 899878912,
|
||||
"cg_oom_kill": 0
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 177
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 177
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 177
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "crashed_after_stream",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 7,
|
||||
"signal": 0,
|
||||
"t": 10362
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 3000,
|
||||
"events_streamed": 10759,
|
||||
"pty_bytes_total": 87107,
|
||||
"pty_data_callbacks": 43,
|
||||
"first_byte_ms": 141,
|
||||
"session_create_ms": 177,
|
||||
"stream_start_ms": 1692,
|
||||
"vmhwm_kb": 899988,
|
||||
"cg_peak": 899878912,
|
||||
"drain_max_loop_lag_ms": 18,
|
||||
"drain_lag_violations": 2,
|
||||
"drain_ok": false,
|
||||
"digest": null
|
||||
},
|
||||
"pty_tail": "urn x}⧉ copy⚕Minim elit mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident. Officia fugiat consequat minim elit mollit. Aliquip aliqua consectetur officia fugiat consequat minim. - Lorem occaecat reprehenderit exercitation incididunt.- Velit laboris magna amet culpa cillum commodo.- Enim adipiscing deserunt nulla.Duis veniam sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore. Adipiscing deserunt nulla duis veniam sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate.◦edit_file Minim elit mollit pariatur. · 2.9s (7 lines)⚡grep Lorem occaecat reprehendet exercitation. · 0s 7⧉ copy ⚕◐Thought: Irure nostrud tempor ⧉ copy ⚕ ⚕ ◐ Thought: Irure nostrud tempor - - - const x3 = 26function f1() { return x}⧉ copy⚕Officia fugiat consequat minim elit mollit pariatur aute quis eiusmod lorem. Aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute quis. Sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur.- Aute quis eiusmod lorem occaecat.- Incididunt dolor proident velit laboris magna amet.- Culpa cillum commodo enim.Adipiscing deserunt nulla duis veniam sed anim excepteur irure nostrud. Cillum commodo enim adipiscing deserunt nulla duis veniam sed anim excepteur.⚡edit_file Officia fugiat consequat minim. · 0s ⧉ copy⚕ ⧉ copy ● web_search Proident velit laboris magna. · 2.5s (18 lines) ◆ write_file Commodo enim adipiscing deserunt. · 2.6s (7 lines) $ terminal Sed anim excepteur irure. · 2.7s (2 lines)◇read_file Cupidatat voluptate ullamco labore. · 2.8s (18 lines)◦edit_file Aliquip aliqua consectetur officia. · 2.9s (7 lines) ⧉ copy ⚕▍ 8 return x} ⧉ copy●web_search Proident velit laboris magna. · 2.5s (18 lines)◆write_file Commodo enim adipiscing deserunt. · 2.6s (7 lines)$terminal Sed anim excepteur irure. · 2.7s (2 lines)◇read_file Cupidatat voluptate ullamco labore. · 2.8s (18 lines)◦edit_file Aliquip aliqua consectetur officia. · 2.9s (7 lines) ⧉ copy ⚕ ⧉ copy ⚕ ◦edit_file Sit sunt esse aliquip. · 0.9s (7 lines)◦grep Fugiat consequat minim elit. · 1.0s (2 lines)●web_search Quis eiusmod lorem occaecat. · 1.1s (18 lines)◆write_file Dolor proident velit laboris. · 1.2s (7 lines)⚡terminal Cillum commodo enim adipiscing. · 0s- const x5 = 47function f2() { return x}Sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute. Voluptate ullamco labore sit sunt esse. Tempor ipsum cupidatat voluptate ullamco labore sit.- Fugiat consequat minim elit mollit.- Quis eiusmod lorem occaecat reprehenderit exercitation incididunt.- Dolor proident velit laboris.Magna amet culpa cillum commodo enim adipiscing deserunt nulla duis veniam sed anim. Proident velit laboris magna amet culpa cillum commodo enim adipiscing deserunt nulla duis veniam.$ terminal1.3s (2 lines)10s │ …/lively-thrush/hermes-agentfile:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915\n throw new Error(`Failed to create optimized buffer: ${width}x${height}`);\n ^\nError: Failed to create optimized buffer: 120x12\n at FFIRenderLib.createOptimizedBuffer (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915:13)\n at OptimizedBuffer.create (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:11918:24)\n at TerminalConsole.show (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:21058:44)\n at CliRenderer.<anonymous> (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:23222:20)\n at process.emit (node:events:509:20)\n at process._fatalException (node:internal/process/execution:190:32)\nNode.js v26.3.0"
|
||||
}
|
||||
@@ -0,0 +1,799 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "mem3000",
|
||||
"ui": "opentui",
|
||||
"config": "otui-uncapped",
|
||||
"mode": "mem",
|
||||
"rep": 0,
|
||||
"run_id": "mq8jwty0-1hks",
|
||||
"utc": "2026-06-10T20:58:44.760Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": "2G",
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": 100000,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-3000.ndjson",
|
||||
"msgs": 3000,
|
||||
"sha256": "0df05a04a611dda68aa07865f21c45b08edc78e0a71d4c8cb2b674729778d96d"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2367592,
|
||||
"gw_pid": 2367601,
|
||||
"cgroup": "/sys/fs/cgroup/user.slice/user-1001.slice/user@1001.service/app.slice/hermes-bench-mq8jwty0-1hks.scope",
|
||||
"load_avg_at_start": [
|
||||
0.71,
|
||||
0.44,
|
||||
0.42
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 51,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 57692,
|
||||
"pss_kb": 31342,
|
||||
"private_dirty_kb": 20212,
|
||||
"vmhwm_kb": 57964,
|
||||
"utime_ticks": 2,
|
||||
"stime_ticks": 1,
|
||||
"cg_current": 22855680,
|
||||
"cg_peak": 22855680,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1056,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 104624,
|
||||
"pss_kb": 62753,
|
||||
"private_dirty_kb": 47868,
|
||||
"vmhwm_kb": 107084,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 61587456,
|
||||
"cg_peak": 65724416,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1709,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 114272,
|
||||
"pss_kb": 63664,
|
||||
"private_dirty_kb": 57388,
|
||||
"vmhwm_kb": 114436,
|
||||
"utime_ticks": 21,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 80728064,
|
||||
"cg_peak": 81027072,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1711,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 114436,
|
||||
"pss_kb": 63828,
|
||||
"private_dirty_kb": 57552,
|
||||
"vmhwm_kb": 114444,
|
||||
"utime_ticks": 21,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 81117184,
|
||||
"cg_peak": 81117184,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1712,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 114536,
|
||||
"pss_kb": 63928,
|
||||
"private_dirty_kb": 57652,
|
||||
"vmhwm_kb": 114536,
|
||||
"utime_ticks": 21,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 80982016,
|
||||
"cg_peak": 81117184,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1713,
|
||||
"msgs": 400,
|
||||
"events": 1432,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 114748,
|
||||
"pss_kb": 64140,
|
||||
"private_dirty_kb": 57864,
|
||||
"vmhwm_kb": 114888,
|
||||
"utime_ticks": 21,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 81768448,
|
||||
"cg_peak": 82030592,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2069,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 218304,
|
||||
"pss_kb": 165381,
|
||||
"private_dirty_kb": 158348,
|
||||
"vmhwm_kb": 218320,
|
||||
"utime_ticks": 74,
|
||||
"stime_ticks": 7,
|
||||
"cg_current": 186974208,
|
||||
"cg_peak": 187183104,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2274,
|
||||
"msgs": 501,
|
||||
"events": 1792,
|
||||
"pty_bytes": 31890,
|
||||
"pty_writes": 15,
|
||||
"rss_kb": 241724,
|
||||
"pss_kb": 188614,
|
||||
"private_dirty_kb": 181708,
|
||||
"vmhwm_kb": 241724,
|
||||
"utime_ticks": 98,
|
||||
"stime_ticks": 8,
|
||||
"cg_current": 211410944,
|
||||
"cg_peak": 212041728,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2551,
|
||||
"msgs": 601,
|
||||
"events": 2154,
|
||||
"pty_bytes": 35126,
|
||||
"pty_writes": 17,
|
||||
"rss_kb": 286064,
|
||||
"pss_kb": 242065,
|
||||
"private_dirty_kb": 225732,
|
||||
"vmhwm_kb": 286084,
|
||||
"utime_ticks": 135,
|
||||
"stime_ticks": 10,
|
||||
"cg_current": 258056192,
|
||||
"cg_peak": 258289664,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2604,
|
||||
"msgs": 701,
|
||||
"events": 2496,
|
||||
"pty_bytes": 36863,
|
||||
"pty_writes": 18,
|
||||
"rss_kb": 321584,
|
||||
"pss_kb": 278638,
|
||||
"private_dirty_kb": 261128,
|
||||
"vmhwm_kb": 321740,
|
||||
"utime_ticks": 151,
|
||||
"stime_ticks": 12,
|
||||
"cg_current": 295292928,
|
||||
"cg_peak": 295944192,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2826,
|
||||
"msgs": 801,
|
||||
"events": 2857,
|
||||
"pty_bytes": 39692,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 338924,
|
||||
"pss_kb": 295973,
|
||||
"private_dirty_kb": 278468,
|
||||
"vmhwm_kb": 344240,
|
||||
"utime_ticks": 187,
|
||||
"stime_ticks": 16,
|
||||
"cg_current": 313622528,
|
||||
"cg_peak": 319459328,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2828,
|
||||
"msgs": 901,
|
||||
"events": 3245,
|
||||
"pty_bytes": 39692,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 338976,
|
||||
"pss_kb": 296025,
|
||||
"private_dirty_kb": 278520,
|
||||
"vmhwm_kb": 344240,
|
||||
"utime_ticks": 187,
|
||||
"stime_ticks": 16,
|
||||
"cg_current": 313614336,
|
||||
"cg_peak": 319459328,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2830,
|
||||
"msgs": 1001,
|
||||
"events": 3588,
|
||||
"pty_bytes": 39692,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 339104,
|
||||
"pss_kb": 296153,
|
||||
"private_dirty_kb": 278648,
|
||||
"vmhwm_kb": 344240,
|
||||
"utime_ticks": 187,
|
||||
"stime_ticks": 16,
|
||||
"cg_current": 313876480,
|
||||
"cg_peak": 319459328,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2832,
|
||||
"msgs": 1101,
|
||||
"events": 3928,
|
||||
"pty_bytes": 39692,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 339404,
|
||||
"pss_kb": 296453,
|
||||
"private_dirty_kb": 278948,
|
||||
"vmhwm_kb": 344240,
|
||||
"utime_ticks": 187,
|
||||
"stime_ticks": 16,
|
||||
"cg_current": 314130432,
|
||||
"cg_peak": 319459328,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2834,
|
||||
"msgs": 1201,
|
||||
"events": 4298,
|
||||
"pty_bytes": 39692,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 339588,
|
||||
"pss_kb": 296637,
|
||||
"private_dirty_kb": 279132,
|
||||
"vmhwm_kb": 344240,
|
||||
"utime_ticks": 187,
|
||||
"stime_ticks": 16,
|
||||
"cg_current": 314392576,
|
||||
"cg_peak": 319459328,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2835,
|
||||
"msgs": 1300,
|
||||
"events": 4659,
|
||||
"pty_bytes": 39692,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 339780,
|
||||
"pss_kb": 296829,
|
||||
"private_dirty_kb": 279324,
|
||||
"vmhwm_kb": 344240,
|
||||
"utime_ticks": 187,
|
||||
"stime_ticks": 16,
|
||||
"cg_current": 314654720,
|
||||
"cg_peak": 319459328,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3078,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 39692,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 425380,
|
||||
"pss_kb": 381530,
|
||||
"private_dirty_kb": 364924,
|
||||
"vmhwm_kb": 425492,
|
||||
"utime_ticks": 219,
|
||||
"stime_ticks": 19,
|
||||
"cg_current": 404750336,
|
||||
"cg_peak": 404750336,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3455,
|
||||
"msgs": 1400,
|
||||
"events": 5011,
|
||||
"pty_bytes": 41169,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 487696,
|
||||
"pss_kb": 443846,
|
||||
"private_dirty_kb": 427240,
|
||||
"vmhwm_kb": 487712,
|
||||
"utime_ticks": 278,
|
||||
"stime_ticks": 22,
|
||||
"cg_current": 469049344,
|
||||
"cg_peak": 469057536,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3757,
|
||||
"msgs": 1500,
|
||||
"events": 5384,
|
||||
"pty_bytes": 41699,
|
||||
"pty_writes": 22,
|
||||
"rss_kb": 519032,
|
||||
"pss_kb": 475182,
|
||||
"private_dirty_kb": 458576,
|
||||
"vmhwm_kb": 519036,
|
||||
"utime_ticks": 310,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 501985280,
|
||||
"cg_peak": 501985280,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4083,
|
||||
"msgs": 1600,
|
||||
"events": 5730,
|
||||
"pty_bytes": 43564,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 546612,
|
||||
"pss_kb": 502762,
|
||||
"private_dirty_kb": 486156,
|
||||
"vmhwm_kb": 546680,
|
||||
"utime_ticks": 345,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 531128320,
|
||||
"cg_peak": 531128320,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4087,
|
||||
"msgs": 1700,
|
||||
"events": 6100,
|
||||
"pty_bytes": 43564,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 546828,
|
||||
"pss_kb": 502978,
|
||||
"private_dirty_kb": 486372,
|
||||
"vmhwm_kb": 546896,
|
||||
"utime_ticks": 345,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 531390464,
|
||||
"cg_peak": 531390464,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4091,
|
||||
"msgs": 1800,
|
||||
"events": 6455,
|
||||
"pty_bytes": 43564,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 546932,
|
||||
"pss_kb": 503082,
|
||||
"private_dirty_kb": 486476,
|
||||
"vmhwm_kb": 547220,
|
||||
"utime_ticks": 345,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 531914752,
|
||||
"cg_peak": 531914752,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4094,
|
||||
"msgs": 1900,
|
||||
"events": 6838,
|
||||
"pty_bytes": 43564,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 547332,
|
||||
"pss_kb": 503482,
|
||||
"private_dirty_kb": 486876,
|
||||
"vmhwm_kb": 547456,
|
||||
"utime_ticks": 345,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 532176896,
|
||||
"cg_peak": 532176896,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4097,
|
||||
"msgs": 2000,
|
||||
"events": 7151,
|
||||
"pty_bytes": 43564,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 547764,
|
||||
"pss_kb": 503914,
|
||||
"private_dirty_kb": 487308,
|
||||
"vmhwm_kb": 547908,
|
||||
"utime_ticks": 346,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 532439040,
|
||||
"cg_peak": 532439040,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4100,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 43564,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 548212,
|
||||
"pss_kb": 504362,
|
||||
"private_dirty_kb": 487756,
|
||||
"vmhwm_kb": 548288,
|
||||
"utime_ticks": 346,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 532963328,
|
||||
"cg_peak": 532963328,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4411,
|
||||
"msgs": 2100,
|
||||
"events": 7532,
|
||||
"pty_bytes": 43564,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 658764,
|
||||
"pss_kb": 614914,
|
||||
"private_dirty_kb": 598308,
|
||||
"vmhwm_kb": 658832,
|
||||
"utime_ticks": 385,
|
||||
"stime_ticks": 31,
|
||||
"cg_current": 648835072,
|
||||
"cg_peak": 648835072,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4791,
|
||||
"msgs": 2201,
|
||||
"events": 7892,
|
||||
"pty_bytes": 44965,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 692780,
|
||||
"pss_kb": 648930,
|
||||
"private_dirty_kb": 632324,
|
||||
"vmhwm_kb": 692908,
|
||||
"utime_ticks": 424,
|
||||
"stime_ticks": 32,
|
||||
"cg_current": 683880448,
|
||||
"cg_peak": 683880448,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 5114,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 44965,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 719796,
|
||||
"pss_kb": 675946,
|
||||
"private_dirty_kb": 659340,
|
||||
"vmhwm_kb": 719796,
|
||||
"utime_ticks": 456,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 711639040,
|
||||
"cg_peak": 711864320,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5468,
|
||||
"msgs": 2301,
|
||||
"events": 8254,
|
||||
"pty_bytes": 47938,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 724840,
|
||||
"pss_kb": 680990,
|
||||
"private_dirty_kb": 664384,
|
||||
"vmhwm_kb": 725152,
|
||||
"utime_ticks": 494,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 717672448,
|
||||
"cg_peak": 717672448,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 6121,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 49397,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 753180,
|
||||
"pss_kb": 709330,
|
||||
"private_dirty_kb": 692724,
|
||||
"vmhwm_kb": 753180,
|
||||
"utime_ticks": 562,
|
||||
"stime_ticks": 38,
|
||||
"cg_current": 746872832,
|
||||
"cg_peak": 747012096,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 6222,
|
||||
"msgs": 2401,
|
||||
"events": 8596,
|
||||
"pty_bytes": 51085,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 756036,
|
||||
"pss_kb": 712186,
|
||||
"private_dirty_kb": 695580,
|
||||
"vmhwm_kb": 756092,
|
||||
"utime_ticks": 572,
|
||||
"stime_ticks": 38,
|
||||
"cg_current": 749625344,
|
||||
"cg_peak": 749625344,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7002,
|
||||
"msgs": 2501,
|
||||
"events": 8957,
|
||||
"pty_bytes": 55291,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 786164,
|
||||
"pss_kb": 742314,
|
||||
"private_dirty_kb": 725708,
|
||||
"vmhwm_kb": 786172,
|
||||
"utime_ticks": 651,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 781586432,
|
||||
"cg_peak": 781586432,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 7127,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 55291,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 807644,
|
||||
"pss_kb": 763794,
|
||||
"private_dirty_kb": 747188,
|
||||
"vmhwm_kb": 807644,
|
||||
"utime_ticks": 664,
|
||||
"stime_ticks": 42,
|
||||
"cg_current": 803647488,
|
||||
"cg_peak": 803848192,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7830,
|
||||
"msgs": 2601,
|
||||
"events": 9345,
|
||||
"pty_bytes": 58421,
|
||||
"pty_writes": 33,
|
||||
"rss_kb": 817624,
|
||||
"pss_kb": 773774,
|
||||
"private_dirty_kb": 757168,
|
||||
"vmhwm_kb": 817692,
|
||||
"utime_ticks": 735,
|
||||
"stime_ticks": 43,
|
||||
"cg_current": 814092288,
|
||||
"cg_peak": 814092288,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 8130,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 58421,
|
||||
"pty_writes": 33,
|
||||
"rss_kb": 842436,
|
||||
"pss_kb": 798586,
|
||||
"private_dirty_kb": 781980,
|
||||
"vmhwm_kb": 842436,
|
||||
"utime_ticks": 764,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 839323648,
|
||||
"cg_peak": 840339456,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 8409,
|
||||
"msgs": 2701,
|
||||
"events": 9688,
|
||||
"pty_bytes": 59910,
|
||||
"pty_writes": 34,
|
||||
"rss_kb": 844204,
|
||||
"pss_kb": 800354,
|
||||
"private_dirty_kb": 783748,
|
||||
"vmhwm_kb": 844204,
|
||||
"utime_ticks": 793,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 841596928,
|
||||
"cg_peak": 841596928,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 9134,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 61517,
|
||||
"pty_writes": 35,
|
||||
"rss_kb": 875368,
|
||||
"pss_kb": 831518,
|
||||
"private_dirty_kb": 814912,
|
||||
"vmhwm_kb": 875368,
|
||||
"utime_ticks": 866,
|
||||
"stime_ticks": 48,
|
||||
"cg_current": 874221568,
|
||||
"cg_peak": 874221568,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9286,
|
||||
"msgs": 2801,
|
||||
"events": 10028,
|
||||
"pty_bytes": 63074,
|
||||
"pty_writes": 36,
|
||||
"rss_kb": 875868,
|
||||
"pss_kb": 832018,
|
||||
"private_dirty_kb": 815412,
|
||||
"vmhwm_kb": 875868,
|
||||
"utime_ticks": 882,
|
||||
"stime_ticks": 48,
|
||||
"cg_current": 874745856,
|
||||
"cg_peak": 874749952,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 10138,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 64846,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 906312,
|
||||
"pss_kb": 862462,
|
||||
"private_dirty_kb": 845856,
|
||||
"vmhwm_kb": 906312,
|
||||
"utime_ticks": 968,
|
||||
"stime_ticks": 51,
|
||||
"cg_current": 906104832,
|
||||
"cg_peak": 906543104,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 10191,
|
||||
"msgs": 2901,
|
||||
"events": 10398,
|
||||
"pty_bytes": 68029,
|
||||
"pty_writes": 39,
|
||||
"rss_kb": 908240,
|
||||
"pss_kb": 864390,
|
||||
"private_dirty_kb": 847784,
|
||||
"vmhwm_kb": 908264,
|
||||
"utime_ticks": 973,
|
||||
"stime_ticks": 51,
|
||||
"cg_current": 908509184,
|
||||
"cg_peak": 908607488,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 10198,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 68029,
|
||||
"pty_writes": 39,
|
||||
"rss_kb": 908380,
|
||||
"pss_kb": 864530,
|
||||
"private_dirty_kb": 847924,
|
||||
"vmhwm_kb": 908432,
|
||||
"utime_ticks": 974,
|
||||
"stime_ticks": 51,
|
||||
"cg_current": 908509184,
|
||||
"cg_peak": 908607488,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 10205,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 68029,
|
||||
"pty_writes": 39,
|
||||
"rss_kb": 908828,
|
||||
"pss_kb": 864978,
|
||||
"private_dirty_kb": 848372,
|
||||
"vmhwm_kb": 908852,
|
||||
"utime_ticks": 974,
|
||||
"stime_ticks": 51,
|
||||
"cg_current": 909033472,
|
||||
"cg_peak": 909033472,
|
||||
"cg_oom_kill": 0
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 177
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 203
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 203
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "crashed_after_stream",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 7,
|
||||
"signal": 0,
|
||||
"t": 10266
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 3000,
|
||||
"events_streamed": 10759,
|
||||
"pty_bytes_total": 69292,
|
||||
"pty_data_callbacks": 40,
|
||||
"first_byte_ms": 146,
|
||||
"session_create_ms": 177,
|
||||
"stream_start_ms": 1707,
|
||||
"vmhwm_kb": 908852,
|
||||
"cg_peak": 909033472,
|
||||
"drain_max_loop_lag_ms": 22,
|
||||
"drain_lag_violations": 3,
|
||||
"drain_ok": false,
|
||||
"digest": null
|
||||
},
|
||||
"pty_tail": "() { return x}⧉ copy⚕ ▼ Thinking: Occaecat reprehenderit exercitation│Consectetur officia fugiat consequat minim elit mollit pariatur aute quis eiusmod. Esse aliquip aliqua │ consectetur officia fugiat consequat minim elit mollit pariatur aute. Occaecat reprehenderit exercitation incididunt dolor proident.- Laboris magna amet culpa cillum.- Adipiscing deserunt nulla duis veniam sed anim.- Excepteur irure nostrud tempor.Ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip aliqua consectetur officia fugiat consequat. Irure nostrud tempor ipsum cupidatat voluptate.⚡terminal Occaecat reprehenderit exercitation incididunt. · 0s ⚕ ⧉ copy ⚕▼Thinking: Quis eiusmod lorem │ │ - - - const x4 = 55function f0() { return x}⧉ copy⚕ ▼ Thinking: Quis eiusmod lorem│Esse aliquip aliqua consectetur officia fugiat consequat minim. Labore sit sunt esse aliquip aliqua │ consectetur officia fugiat. Quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna.- Dolor proident velit laboris magna.- Cillum commodo enim adipiscing deserunt nulla duis.- Veniam sed anim excepteur.Irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse. Sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit.⚡terminal Quis eiusmod lorem occaecat. · 0s8⧉ copy⚕◐Thought: Deserunt nulla duis ⧉ copy ⚕ ● web_search Irure nostrud tempor ipsum. · 2.1s (18 lines)⚕ ◐ Thought: Deserunt nulla duis - - - const x5 = 19function f4() { return x}⧉ copy⚕Irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip aliqua. Sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse. - Labore sit sunt esse aliquip.- Officia fugiat consequat minim elit mollit pariatur.- Aute quis eiusmod lorem.Occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna amet culpa cillum. Quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna amet.●web_search Irure nostrud tempor ipsum. · 2.1s (18 lines)⚡write_fileLabore si sun esse. · 0s ⧉ copy ⚕ ▼ Thinking: Consequat minim elit │ │ $terminal Consequat minim elit mollit. · 3.7s (2 lines) ◇ read_file Eiusmod lorem occaecat reprehenderit. · 3.8s (18 lines) ◦edit_file Proidnt velitlaboris magna. · 3.9s (7gep Commodo enimadipiscing dserunt. · 0s9const x5 = 75function f0() { return x}Cupidatat voluptate ullamco labore sit sunt esse aliquip aliqua consectetur officia. Nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip aliqua.Consequat minim elit mollit pariatur aute.- Eiusmod lorem occaecat reprehenderit exercitation.- Proident velit laboris magna amet culpa cillum.- Commodo enim adipiscing deserunt.Nulla duis veniam sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore. Enim adipiscingdeserunt nulla duis veniam.◦ grep4.0s (2 lines)file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915\n throw new Error(`Failed to create optimized buffer: ${width}x${height}`);\n ^\nError: Failed to create optimized buffer: 120x12\n at FFIRenderLib.createOptimizedBuffer (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915:13)\n at OptimizedBuffer.create (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:11918:24)\n at TerminalConsole.show (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:21058:44)\n at CliRenderer.<anonymous> (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:23222:20)\n at process.emit (node:events:509:20)\n at process._fatalException (node:internal/process/execution:190:32)\nNode.js v26.3.0"
|
||||
}
|
||||
@@ -0,0 +1,782 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "mem3000",
|
||||
"ui": "opentui",
|
||||
"config": "otui-uncapped",
|
||||
"mode": "mem",
|
||||
"rep": 1,
|
||||
"run_id": "mq8jy0dw-48jl",
|
||||
"utc": "2026-06-10T20:59:39.764Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": "2G",
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": 100000,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-3000.ndjson",
|
||||
"msgs": 3000,
|
||||
"sha256": "0df05a04a611dda68aa07865f21c45b08edc78e0a71d4c8cb2b674729778d96d"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2369054,
|
||||
"gw_pid": 2369063,
|
||||
"cgroup": "/sys/fs/cgroup/user.slice/user-1001.slice/session-7349.scope",
|
||||
"load_avg_at_start": [
|
||||
0.97,
|
||||
0.58,
|
||||
0.47
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 26,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 26616,
|
||||
"pss_kb": 9810,
|
||||
"private_dirty_kb": 3112,
|
||||
"vmhwm_kb": 26616,
|
||||
"utime_ticks": 0,
|
||||
"stime_ticks": 0,
|
||||
"cg_current": 3192561664,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1032,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 11,
|
||||
"rss_kb": 105124,
|
||||
"pss_kb": 64072,
|
||||
"private_dirty_kb": 48416,
|
||||
"vmhwm_kb": 107976,
|
||||
"utime_ticks": 18,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3193053184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1711,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 113336,
|
||||
"pss_kb": 72247,
|
||||
"private_dirty_kb": 56628,
|
||||
"vmhwm_kb": 113472,
|
||||
"utime_ticks": 20,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3192659968,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1712,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 113720,
|
||||
"pss_kb": 72567,
|
||||
"private_dirty_kb": 56884,
|
||||
"vmhwm_kb": 113772,
|
||||
"utime_ticks": 21,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3192659968,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1713,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 113976,
|
||||
"pss_kb": 72823,
|
||||
"private_dirty_kb": 57140,
|
||||
"vmhwm_kb": 114008,
|
||||
"utime_ticks": 21,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3192659968,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1713,
|
||||
"msgs": 400,
|
||||
"events": 1432,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 114224,
|
||||
"pss_kb": 73071,
|
||||
"private_dirty_kb": 57388,
|
||||
"vmhwm_kb": 114240,
|
||||
"utime_ticks": 21,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3192659968,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2039,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 233888,
|
||||
"pss_kb": 190414,
|
||||
"private_dirty_kb": 173920,
|
||||
"vmhwm_kb": 233908,
|
||||
"utime_ticks": 75,
|
||||
"stime_ticks": 7,
|
||||
"cg_current": 3192918016,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2139,
|
||||
"msgs": 501,
|
||||
"events": 1792,
|
||||
"pty_bytes": 31881,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 242104,
|
||||
"pss_kb": 198630,
|
||||
"private_dirty_kb": 182136,
|
||||
"vmhwm_kb": 242104,
|
||||
"utime_ticks": 86,
|
||||
"stime_ticks": 8,
|
||||
"cg_current": 3192664064,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2341,
|
||||
"msgs": 601,
|
||||
"events": 2154,
|
||||
"pty_bytes": 36276,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 277724,
|
||||
"pss_kb": 234112,
|
||||
"private_dirty_kb": 217568,
|
||||
"vmhwm_kb": 277744,
|
||||
"utime_ticks": 113,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3192410112,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2415,
|
||||
"msgs": 701,
|
||||
"events": 2496,
|
||||
"pty_bytes": 38144,
|
||||
"pty_writes": 17,
|
||||
"rss_kb": 293016,
|
||||
"pss_kb": 249217,
|
||||
"private_dirty_kb": 232608,
|
||||
"vmhwm_kb": 319420,
|
||||
"utime_ticks": 136,
|
||||
"stime_ticks": 11,
|
||||
"cg_current": 3192414208,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2665,
|
||||
"msgs": 801,
|
||||
"events": 2857,
|
||||
"pty_bytes": 42361,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 347140,
|
||||
"pss_kb": 303341,
|
||||
"private_dirty_kb": 286732,
|
||||
"vmhwm_kb": 347192,
|
||||
"utime_ticks": 174,
|
||||
"stime_ticks": 13,
|
||||
"cg_current": 3192664064,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2890,
|
||||
"msgs": 901,
|
||||
"events": 3245,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 378252,
|
||||
"pss_kb": 334448,
|
||||
"private_dirty_kb": 317844,
|
||||
"vmhwm_kb": 378328,
|
||||
"utime_ticks": 200,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3191242752,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2894,
|
||||
"msgs": 1001,
|
||||
"events": 3588,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 378556,
|
||||
"pss_kb": 334752,
|
||||
"private_dirty_kb": 318148,
|
||||
"vmhwm_kb": 378632,
|
||||
"utime_ticks": 201,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3191500800,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2896,
|
||||
"msgs": 1101,
|
||||
"events": 3928,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 378840,
|
||||
"pss_kb": 335036,
|
||||
"private_dirty_kb": 318432,
|
||||
"vmhwm_kb": 378944,
|
||||
"utime_ticks": 201,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3191242752,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2898,
|
||||
"msgs": 1201,
|
||||
"events": 4298,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 379096,
|
||||
"pss_kb": 335292,
|
||||
"private_dirty_kb": 318688,
|
||||
"vmhwm_kb": 379164,
|
||||
"utime_ticks": 201,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3191242752,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2899,
|
||||
"msgs": 1300,
|
||||
"events": 4659,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 379292,
|
||||
"pss_kb": 335488,
|
||||
"private_dirty_kb": 318884,
|
||||
"vmhwm_kb": 379340,
|
||||
"utime_ticks": 201,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3191242752,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2900,
|
||||
"msgs": 1400,
|
||||
"events": 5011,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 379448,
|
||||
"pss_kb": 335644,
|
||||
"private_dirty_kb": 319040,
|
||||
"vmhwm_kb": 379464,
|
||||
"utime_ticks": 201,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3191242752,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2902,
|
||||
"msgs": 1500,
|
||||
"events": 5384,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 379524,
|
||||
"pss_kb": 335720,
|
||||
"private_dirty_kb": 319116,
|
||||
"vmhwm_kb": 379600,
|
||||
"utime_ticks": 201,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3191242752,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3041,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 429064,
|
||||
"pss_kb": 385260,
|
||||
"private_dirty_kb": 368656,
|
||||
"vmhwm_kb": 432296,
|
||||
"utime_ticks": 222,
|
||||
"stime_ticks": 17,
|
||||
"cg_current": 3191476224,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3342,
|
||||
"msgs": 1600,
|
||||
"events": 5730,
|
||||
"pty_bytes": 45955,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 520268,
|
||||
"pss_kb": 476464,
|
||||
"private_dirty_kb": 459860,
|
||||
"vmhwm_kb": 520268,
|
||||
"utime_ticks": 280,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3191271424,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4018,
|
||||
"msgs": 1700,
|
||||
"events": 6100,
|
||||
"pty_bytes": 48813,
|
||||
"pty_writes": 23,
|
||||
"rss_kb": 595392,
|
||||
"pss_kb": 551588,
|
||||
"private_dirty_kb": 534984,
|
||||
"vmhwm_kb": 595392,
|
||||
"utime_ticks": 350,
|
||||
"stime_ticks": 22,
|
||||
"cg_current": 3191713792,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4044,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 48813,
|
||||
"pty_writes": 23,
|
||||
"rss_kb": 595392,
|
||||
"pss_kb": 551588,
|
||||
"private_dirty_kb": 534984,
|
||||
"vmhwm_kb": 595392,
|
||||
"utime_ticks": 353,
|
||||
"stime_ticks": 22,
|
||||
"cg_current": 3191713792,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4195,
|
||||
"msgs": 1800,
|
||||
"events": 6455,
|
||||
"pty_bytes": 50669,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 597364,
|
||||
"pss_kb": 553560,
|
||||
"private_dirty_kb": 536956,
|
||||
"vmhwm_kb": 597404,
|
||||
"utime_ticks": 369,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 3191312384,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4646,
|
||||
"msgs": 1900,
|
||||
"events": 6838,
|
||||
"pty_bytes": 53455,
|
||||
"pty_writes": 26,
|
||||
"rss_kb": 629256,
|
||||
"pss_kb": 585452,
|
||||
"private_dirty_kb": 568848,
|
||||
"vmhwm_kb": 629392,
|
||||
"utime_ticks": 416,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 3192115200,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4662,
|
||||
"msgs": 2000,
|
||||
"events": 7151,
|
||||
"pty_bytes": 53455,
|
||||
"pty_writes": 26,
|
||||
"rss_kb": 629784,
|
||||
"pss_kb": 585980,
|
||||
"private_dirty_kb": 569376,
|
||||
"vmhwm_kb": 629888,
|
||||
"utime_ticks": 417,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 3192115200,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4668,
|
||||
"msgs": 2100,
|
||||
"events": 7532,
|
||||
"pty_bytes": 53455,
|
||||
"pty_writes": 26,
|
||||
"rss_kb": 630020,
|
||||
"pss_kb": 586216,
|
||||
"private_dirty_kb": 569612,
|
||||
"vmhwm_kb": 630032,
|
||||
"utime_ticks": 417,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 3192377344,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4672,
|
||||
"msgs": 2201,
|
||||
"events": 7892,
|
||||
"pty_bytes": 53455,
|
||||
"pty_writes": 26,
|
||||
"rss_kb": 630268,
|
||||
"pss_kb": 586464,
|
||||
"private_dirty_kb": 569860,
|
||||
"vmhwm_kb": 630344,
|
||||
"utime_ticks": 418,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 3192377344,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4676,
|
||||
"msgs": 2301,
|
||||
"events": 8254,
|
||||
"pty_bytes": 53455,
|
||||
"pty_writes": 26,
|
||||
"rss_kb": 630488,
|
||||
"pss_kb": 586684,
|
||||
"private_dirty_kb": 570080,
|
||||
"vmhwm_kb": 630532,
|
||||
"utime_ticks": 418,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 3192377344,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 5060,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 53455,
|
||||
"pty_writes": 26,
|
||||
"rss_kb": 751880,
|
||||
"pss_kb": 708076,
|
||||
"private_dirty_kb": 691472,
|
||||
"vmhwm_kb": 751948,
|
||||
"utime_ticks": 466,
|
||||
"stime_ticks": 30,
|
||||
"cg_current": 3192451072,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5360,
|
||||
"msgs": 2401,
|
||||
"events": 8596,
|
||||
"pty_bytes": 55789,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 760368,
|
||||
"pss_kb": 716564,
|
||||
"private_dirty_kb": 699960,
|
||||
"vmhwm_kb": 760428,
|
||||
"utime_ticks": 496,
|
||||
"stime_ticks": 30,
|
||||
"cg_current": 3192664064,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 6061,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 57896,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 791384,
|
||||
"pss_kb": 747580,
|
||||
"private_dirty_kb": 730976,
|
||||
"vmhwm_kb": 791384,
|
||||
"utime_ticks": 569,
|
||||
"stime_ticks": 32,
|
||||
"cg_current": 3193569280,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 6112,
|
||||
"msgs": 2501,
|
||||
"events": 8957,
|
||||
"pty_bytes": 60544,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 794628,
|
||||
"pss_kb": 750824,
|
||||
"private_dirty_kb": 734220,
|
||||
"vmhwm_kb": 794632,
|
||||
"utime_ticks": 574,
|
||||
"stime_ticks": 32,
|
||||
"cg_current": 3193577472,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 6664,
|
||||
"msgs": 2601,
|
||||
"events": 9345,
|
||||
"pty_bytes": 63209,
|
||||
"pty_writes": 30,
|
||||
"rss_kb": 821804,
|
||||
"pss_kb": 778000,
|
||||
"private_dirty_kb": 761396,
|
||||
"vmhwm_kb": 821804,
|
||||
"utime_ticks": 630,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 3193458688,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 7066,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 65585,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 847596,
|
||||
"pss_kb": 803792,
|
||||
"private_dirty_kb": 787188,
|
||||
"vmhwm_kb": 847616,
|
||||
"utime_ticks": 671,
|
||||
"stime_ticks": 37,
|
||||
"cg_current": 3193528320,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7492,
|
||||
"msgs": 2701,
|
||||
"events": 9688,
|
||||
"pty_bytes": 66999,
|
||||
"pty_writes": 32,
|
||||
"rss_kb": 852344,
|
||||
"pss_kb": 808540,
|
||||
"private_dirty_kb": 791936,
|
||||
"vmhwm_kb": 852344,
|
||||
"utime_ticks": 714,
|
||||
"stime_ticks": 37,
|
||||
"cg_current": 3193872384,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 8072,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 68029,
|
||||
"pty_writes": 33,
|
||||
"rss_kb": 881744,
|
||||
"pss_kb": 837940,
|
||||
"private_dirty_kb": 821336,
|
||||
"vmhwm_kb": 881744,
|
||||
"utime_ticks": 773,
|
||||
"stime_ticks": 39,
|
||||
"cg_current": 3194839040,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 8372,
|
||||
"msgs": 2801,
|
||||
"events": 10028,
|
||||
"pty_bytes": 70059,
|
||||
"pty_writes": 34,
|
||||
"rss_kb": 883640,
|
||||
"pss_kb": 839835,
|
||||
"private_dirty_kb": 823232,
|
||||
"vmhwm_kb": 883640,
|
||||
"utime_ticks": 804,
|
||||
"stime_ticks": 39,
|
||||
"cg_current": 3199139840,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 9075,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 72544,
|
||||
"pty_writes": 35,
|
||||
"rss_kb": 915004,
|
||||
"pss_kb": 871200,
|
||||
"private_dirty_kb": 854596,
|
||||
"vmhwm_kb": 915004,
|
||||
"utime_ticks": 874,
|
||||
"stime_ticks": 43,
|
||||
"cg_current": 3200159744,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9299,
|
||||
"msgs": 2901,
|
||||
"events": 10398,
|
||||
"pty_bytes": 74276,
|
||||
"pty_writes": 36,
|
||||
"rss_kb": 915420,
|
||||
"pss_kb": 871616,
|
||||
"private_dirty_kb": 855012,
|
||||
"vmhwm_kb": 915428,
|
||||
"utime_ticks": 898,
|
||||
"stime_ticks": 43,
|
||||
"cg_current": 3199324160,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9325,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 75521,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 917560,
|
||||
"pss_kb": 873756,
|
||||
"private_dirty_kb": 857152,
|
||||
"vmhwm_kb": 917668,
|
||||
"utime_ticks": 901,
|
||||
"stime_ticks": 43,
|
||||
"cg_current": 3199324160,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 9332,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 75521,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 917692,
|
||||
"pss_kb": 873888,
|
||||
"private_dirty_kb": 857284,
|
||||
"vmhwm_kb": 917736,
|
||||
"utime_ticks": 901,
|
||||
"stime_ticks": 43,
|
||||
"cg_current": 3199324160,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 201
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "crashed_after_stream",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 7,
|
||||
"signal": 0,
|
||||
"t": 9384
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 3000,
|
||||
"events_streamed": 10759,
|
||||
"pty_bytes_total": 76784,
|
||||
"pty_data_callbacks": 38,
|
||||
"first_byte_ms": 153,
|
||||
"session_create_ms": 201,
|
||||
"stream_start_ms": 1709,
|
||||
"vmhwm_kb": 917736,
|
||||
"cg_peak": 6536753152,
|
||||
"drain_max_loop_lag_ms": 33,
|
||||
"drain_lag_violations": 3,
|
||||
"drain_ok": false,
|
||||
"digest": null
|
||||
},
|
||||
"pty_tail": "nes)⚡write_fileAliquip aliqua consectetur officias $ terminal Occaecat reprehenderit exercitation incididunt. · 1.7s (2 lines) ◇read_file Laboris magna amet culpa. · 1.8s (18 lines) ◦ edit_file Adipiscing deserunt nulla duis. · 1.9s (7 lines) ◦grep Excepteur irure nostrud tempor. · 2.0s (2 lines)●web_search Ullamco labore sit sunt. · 2.1s (18 lines)◆write_file Consectetur officia fugiat consequat. · 2.2s (7 lines) $ terminal Pariatur aute quis eiusmod. · 2.3s (2 lines) ⧉ copy ⚕ ⧉ copy $terminal Occaecat reprehenderit exercitation incididunt. · 1.7s (2 lines) ◇read_file Laboris magna amet culpa. · 1.8s (18 lines) ◦edit_file Adipiscing deserunt nulla duis. · 1.9s (7 lines) ◦grep Excepteur irure nostrud tempor. · 2.0s (2 lines) ●web_search Ullamco labore sit sunt. · 2.1s (18 lines) ◆write_file Consectetur officia fugiat consequat. · 2.2s (7 lines) $terminal Pariatur aute quis eiusmod. · 2.3s (2 lines) ⧉ copy ⚕ - - - const x2 = 97function f2() { return x}⧉ copy●6⧉ copy ⚕ ⧉ copy ⚕ ⧉ copy⚕▍ ◐⚕ - - - const x6 = 57function f2() { return x}⧉ copy⚕ - - - const x0 = 58function f3() { return x}⧉ copy⚕ ▼ Thinking: Veniam sed anim│ │ ◇read_file Aute quis eiusmod lorem. · 2.4s (18 lines) ◦edit_file Incididunt dolor proident velit. · 2.5s (7 lines) ◦grep Culpa cillum commodo enim. · 2.6s (2 lines) ● web_search Duis veniam sed anim. · 2.7s (18 lines) ◆ write_file Tempor ipsum cupidatat voluptate. · 2.8s (7 lines) $terminal Sunt esse aliquip aliqua. · 2.9s (2 lines) ◇ read_file Consequat minim elit mollit. · 3.0s (18 lines) ◦edit_file Eiusmod lorem occaecat reprehenderit. · 3.1s (7 lines) ⧉ copy ⚕ 7◇read_file Aute quis eiusmod lorem. · 2.4s (18 lines) ◦edit_file Incididunt dolor proident velit. · 2.5s (7 lines) ◦grep Culpa cillum commodo enim. · 2.6s (2 lines) ●web_search Duis veniam sed anim. · 2.7s (18 lines) ◆write_file Tempor ipsum cupidatat voluptate. · 2.8s (7 lines) $terminal Sunt esse aliquip aliqua. · 2.9s (2 lines) ◇read_file Consequat minim elit mollit. · 3.0s (18 lines) ◦edit_file Eiusmod lorem occaecat reprehenderit. · 3.1s (7 lines) ⧉ copy ⚕ - - - const x0 = 21function f1() { return x}⧉ copy● ⧉ copy ⚕ ◦ edit_file Occaecat reprehenderit exercitation incididunt. · 0.5s (7 lines) ◦grep Laboris magna amet culpa. · 0.6s (2 lines) ⚡web_search Adipiscing deserunt nulla duis. · 0s◐- - - const x6 = 83function f3() { return x}Occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna amet culpa cillum commodo enim. Quis eiusmod lorem occaecat reprehenderit exercitation. Mollit pariatur aute quis eiusmod lorem occaecat.- Laboris magna amet culpa cillum.- Adipiscing deserunt nulla duis veniam sed anim.- Excepteur irure nostrud tempor.Ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip aliqua consectetur officia fugiat. Irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip aliqua consectetur.● web_search.7s (18 lines)9file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915\n throw new Error(`Failed to create optimized buffer: ${width}x${height}`);\n ^\nError: Failed to create optimized buffer: 120x12\n at FFIRenderLib.createOptimizedBuffer (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915:13)\n at OptimizedBuffer.create (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:11918:24)\n at TerminalConsole.show (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:21058:44)\n at CliRenderer.<anonymous> (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:23222:20)\n at process.emit (node:events:509:20)\n at process._fatalException (node:internal/process/execution:190:32)\nNode.js v26.3.0"
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "mem3000",
|
||||
"ui": "opentui",
|
||||
"config": "otui-capped",
|
||||
"mode": "mem",
|
||||
"rep": 1,
|
||||
"run_id": "mq8jyfi3-9iyw",
|
||||
"utc": "2026-06-10T20:59:59.356Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": "2G",
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": 3000,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-3000.ndjson",
|
||||
"msgs": 3000,
|
||||
"sha256": "0df05a04a611dda68aa07865f21c45b08edc78e0a71d4c8cb2b674729778d96d"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2369510,
|
||||
"gw_pid": 2369523,
|
||||
"cgroup": "/sys/fs/cgroup/user.slice/user-1001.slice/session-7349.scope",
|
||||
"load_avg_at_start": [
|
||||
0.85,
|
||||
0.57,
|
||||
0.47
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 25,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 26620,
|
||||
"pss_kb": 9806,
|
||||
"private_dirty_kb": 3108,
|
||||
"vmhwm_kb": 26620,
|
||||
"utime_ticks": 0,
|
||||
"stime_ticks": 0,
|
||||
"cg_current": 3198296064,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1033,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 11,
|
||||
"rss_kb": 105328,
|
||||
"pss_kb": 64253,
|
||||
"private_dirty_kb": 48596,
|
||||
"vmhwm_kb": 107756,
|
||||
"utime_ticks": 20,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 3198902272,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1692,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 106572,
|
||||
"pss_kb": 65476,
|
||||
"private_dirty_kb": 49840,
|
||||
"vmhwm_kb": 107756,
|
||||
"utime_ticks": 21,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 3199021056,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1721,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 117112,
|
||||
"pss_kb": 75936,
|
||||
"private_dirty_kb": 60252,
|
||||
"vmhwm_kb": 117112,
|
||||
"utime_ticks": 27,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3199021056,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1722,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 117120,
|
||||
"pss_kb": 75944,
|
||||
"private_dirty_kb": 60260,
|
||||
"vmhwm_kb": 117124,
|
||||
"utime_ticks": 28,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3199021056,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1723,
|
||||
"msgs": 400,
|
||||
"events": 1432,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 117148,
|
||||
"pss_kb": 75972,
|
||||
"private_dirty_kb": 60288,
|
||||
"vmhwm_kb": 117152,
|
||||
"utime_ticks": 28,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3199021056,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1724,
|
||||
"msgs": 501,
|
||||
"events": 1792,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 117160,
|
||||
"pss_kb": 75984,
|
||||
"private_dirty_kb": 60300,
|
||||
"vmhwm_kb": 117164,
|
||||
"utime_ticks": 28,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3199021056,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2046,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 240808,
|
||||
"pss_kb": 197341,
|
||||
"private_dirty_kb": 180876,
|
||||
"vmhwm_kb": 240824,
|
||||
"utime_ticks": 81,
|
||||
"stime_ticks": 7,
|
||||
"cg_current": 3199107072,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2245,
|
||||
"msgs": 601,
|
||||
"events": 2154,
|
||||
"pty_bytes": 37880,
|
||||
"pty_writes": 17,
|
||||
"rss_kb": 257716,
|
||||
"pss_kb": 214080,
|
||||
"private_dirty_kb": 197536,
|
||||
"vmhwm_kb": 257812,
|
||||
"utime_ticks": 106,
|
||||
"stime_ticks": 8,
|
||||
"cg_current": 3199311872,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2422,
|
||||
"msgs": 701,
|
||||
"events": 2496,
|
||||
"pty_bytes": 40363,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 290072,
|
||||
"pss_kb": 246426,
|
||||
"private_dirty_kb": 229892,
|
||||
"vmhwm_kb": 290428,
|
||||
"utime_ticks": 128,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3200040960,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2447,
|
||||
"msgs": 801,
|
||||
"events": 2857,
|
||||
"pty_bytes": 41355,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 298000,
|
||||
"pss_kb": 254285,
|
||||
"private_dirty_kb": 237692,
|
||||
"vmhwm_kb": 298144,
|
||||
"utime_ticks": 136,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3199799296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2450,
|
||||
"msgs": 901,
|
||||
"events": 3245,
|
||||
"pty_bytes": 41355,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 298400,
|
||||
"pss_kb": 254685,
|
||||
"private_dirty_kb": 238092,
|
||||
"vmhwm_kb": 297592,
|
||||
"utime_ticks": 137,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3199799296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2452,
|
||||
"msgs": 1001,
|
||||
"events": 3588,
|
||||
"pty_bytes": 41355,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 295160,
|
||||
"pss_kb": 251445,
|
||||
"private_dirty_kb": 234852,
|
||||
"vmhwm_kb": 297592,
|
||||
"utime_ticks": 137,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3199799296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2454,
|
||||
"msgs": 1101,
|
||||
"events": 3928,
|
||||
"pty_bytes": 41355,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 295568,
|
||||
"pss_kb": 251853,
|
||||
"private_dirty_kb": 235260,
|
||||
"vmhwm_kb": 297592,
|
||||
"utime_ticks": 137,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3199799296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2456,
|
||||
"msgs": 1201,
|
||||
"events": 4298,
|
||||
"pty_bytes": 41355,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 295800,
|
||||
"pss_kb": 252085,
|
||||
"private_dirty_kb": 235492,
|
||||
"vmhwm_kb": 297592,
|
||||
"utime_ticks": 137,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3199799296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2457,
|
||||
"msgs": 1300,
|
||||
"events": 4659,
|
||||
"pty_bytes": 41355,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 296056,
|
||||
"pss_kb": 252341,
|
||||
"private_dirty_kb": 235748,
|
||||
"vmhwm_kb": 297592,
|
||||
"utime_ticks": 138,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3199799296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2903,
|
||||
"msgs": 1400,
|
||||
"events": 5011,
|
||||
"pty_bytes": 41355,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 468980,
|
||||
"pss_kb": 425153,
|
||||
"private_dirty_kb": 408548,
|
||||
"vmhwm_kb": 468996,
|
||||
"utime_ticks": 235,
|
||||
"stime_ticks": 18,
|
||||
"cg_current": 3200602112,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3054,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 41355,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 499912,
|
||||
"pss_kb": 456085,
|
||||
"private_dirty_kb": 439480,
|
||||
"vmhwm_kb": 499928,
|
||||
"utime_ticks": 250,
|
||||
"stime_ticks": 18,
|
||||
"cg_current": 3200593920,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3534,
|
||||
"msgs": 1500,
|
||||
"events": 5384,
|
||||
"pty_bytes": 44170,
|
||||
"pty_writes": 22,
|
||||
"rss_kb": 534956,
|
||||
"pss_kb": 491129,
|
||||
"private_dirty_kb": 474524,
|
||||
"vmhwm_kb": 534956,
|
||||
"utime_ticks": 300,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3201155072,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3684,
|
||||
"msgs": 1600,
|
||||
"events": 5730,
|
||||
"pty_bytes": 46055,
|
||||
"pty_writes": 23,
|
||||
"rss_kb": 535820,
|
||||
"pss_kb": 491993,
|
||||
"private_dirty_kb": 475388,
|
||||
"vmhwm_kb": 535868,
|
||||
"utime_ticks": 317,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3200622592,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4060,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 48276,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 565124,
|
||||
"pss_kb": 521297,
|
||||
"private_dirty_kb": 504692,
|
||||
"vmhwm_kb": 565124,
|
||||
"utime_ticks": 355,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 3201978368,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4237,
|
||||
"msgs": 1700,
|
||||
"events": 6100,
|
||||
"pty_bytes": 50759,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 570876,
|
||||
"pss_kb": 527049,
|
||||
"private_dirty_kb": 510444,
|
||||
"vmhwm_kb": 570904,
|
||||
"utime_ticks": 375,
|
||||
"stime_ticks": 23,
|
||||
"cg_current": 3201060864,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4787,
|
||||
"msgs": 1800,
|
||||
"events": 6455,
|
||||
"pty_bytes": 55578,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 600836,
|
||||
"pss_kb": 557009,
|
||||
"private_dirty_kb": 540404,
|
||||
"vmhwm_kb": 601100,
|
||||
"utime_ticks": 432,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 3201822720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 5063,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 55578,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 630232,
|
||||
"pss_kb": 586405,
|
||||
"private_dirty_kb": 569800,
|
||||
"vmhwm_kb": 630320,
|
||||
"utime_ticks": 460,
|
||||
"stime_ticks": 28,
|
||||
"cg_current": 3202007040,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5392,
|
||||
"msgs": 1900,
|
||||
"events": 6838,
|
||||
"pty_bytes": 60725,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 632396,
|
||||
"pss_kb": 588569,
|
||||
"private_dirty_kb": 571964,
|
||||
"vmhwm_kb": 632616,
|
||||
"utime_ticks": 493,
|
||||
"stime_ticks": 28,
|
||||
"cg_current": 3202117632,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5844,
|
||||
"msgs": 2000,
|
||||
"events": 7151,
|
||||
"pty_bytes": 63787,
|
||||
"pty_writes": 30,
|
||||
"rss_kb": 663860,
|
||||
"pss_kb": 620033,
|
||||
"private_dirty_kb": 603428,
|
||||
"vmhwm_kb": 663860,
|
||||
"utime_ticks": 539,
|
||||
"stime_ticks": 30,
|
||||
"cg_current": 3202842624,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 6070,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 66765,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 668208,
|
||||
"pss_kb": 624381,
|
||||
"private_dirty_kb": 607776,
|
||||
"vmhwm_kb": 668252,
|
||||
"utime_ticks": 562,
|
||||
"stime_ticks": 31,
|
||||
"cg_current": 3203407872,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 6520,
|
||||
"msgs": 2100,
|
||||
"events": 7532,
|
||||
"pty_bytes": 68991,
|
||||
"pty_writes": 32,
|
||||
"rss_kb": 693816,
|
||||
"pss_kb": 649989,
|
||||
"private_dirty_kb": 633384,
|
||||
"vmhwm_kb": 693816,
|
||||
"utime_ticks": 608,
|
||||
"stime_ticks": 33,
|
||||
"cg_current": 3203493888,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 7075,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 71150,
|
||||
"pty_writes": 33,
|
||||
"rss_kb": 723308,
|
||||
"pss_kb": 679481,
|
||||
"private_dirty_kb": 662876,
|
||||
"vmhwm_kb": 723308,
|
||||
"utime_ticks": 665,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 3203358720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7226,
|
||||
"msgs": 2201,
|
||||
"events": 7892,
|
||||
"pty_bytes": 72878,
|
||||
"pty_writes": 34,
|
||||
"rss_kb": 723708,
|
||||
"pss_kb": 679881,
|
||||
"private_dirty_kb": 663276,
|
||||
"vmhwm_kb": 723708,
|
||||
"utime_ticks": 681,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 3203919872,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7377,
|
||||
"msgs": 2301,
|
||||
"events": 8254,
|
||||
"pty_bytes": 73927,
|
||||
"pty_writes": 35,
|
||||
"rss_kb": 727704,
|
||||
"pss_kb": 683877,
|
||||
"private_dirty_kb": 667272,
|
||||
"vmhwm_kb": 727712,
|
||||
"utime_ticks": 696,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 3203964928,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7878,
|
||||
"msgs": 2401,
|
||||
"events": 8596,
|
||||
"pty_bytes": 76342,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 755644,
|
||||
"pss_kb": 711817,
|
||||
"private_dirty_kb": 695212,
|
||||
"vmhwm_kb": 755748,
|
||||
"utime_ticks": 750,
|
||||
"stime_ticks": 37,
|
||||
"cg_current": 3204108288,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7903,
|
||||
"msgs": 2501,
|
||||
"events": 8957,
|
||||
"pty_bytes": 76342,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 758700,
|
||||
"pss_kb": 714873,
|
||||
"private_dirty_kb": 698268,
|
||||
"vmhwm_kb": 758712,
|
||||
"utime_ticks": 752,
|
||||
"stime_ticks": 37,
|
||||
"cg_current": 3204087808,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7908,
|
||||
"msgs": 2601,
|
||||
"events": 9345,
|
||||
"pty_bytes": 76342,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 758724,
|
||||
"pss_kb": 714897,
|
||||
"private_dirty_kb": 698292,
|
||||
"vmhwm_kb": 758748,
|
||||
"utime_ticks": 752,
|
||||
"stime_ticks": 37,
|
||||
"cg_current": 3204087808,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 8078,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 76342,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 820220,
|
||||
"pss_kb": 776393,
|
||||
"private_dirty_kb": 759788,
|
||||
"vmhwm_kb": 820220,
|
||||
"utime_ticks": 774,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 3204931584,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 8532,
|
||||
"msgs": 2701,
|
||||
"events": 9688,
|
||||
"pty_bytes": 77954,
|
||||
"pty_writes": 38,
|
||||
"rss_kb": 846112,
|
||||
"pss_kb": 802285,
|
||||
"private_dirty_kb": 785680,
|
||||
"vmhwm_kb": 846184,
|
||||
"utime_ticks": 819,
|
||||
"stime_ticks": 41,
|
||||
"cg_current": 3204521984,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9009,
|
||||
"msgs": 2801,
|
||||
"events": 10028,
|
||||
"pty_bytes": 78613,
|
||||
"pty_writes": 39,
|
||||
"rss_kb": 868832,
|
||||
"pss_kb": 825004,
|
||||
"private_dirty_kb": 808400,
|
||||
"vmhwm_kb": 868832,
|
||||
"utime_ticks": 867,
|
||||
"stime_ticks": 43,
|
||||
"cg_current": 3215708160,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 9085,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 78613,
|
||||
"pty_writes": 39,
|
||||
"rss_kb": 869044,
|
||||
"pss_kb": 825216,
|
||||
"private_dirty_kb": 808612,
|
||||
"vmhwm_kb": 869044,
|
||||
"utime_ticks": 875,
|
||||
"stime_ticks": 43,
|
||||
"cg_current": 3212128256,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9913,
|
||||
"msgs": 2901,
|
||||
"events": 10398,
|
||||
"pty_bytes": 81674,
|
||||
"pty_writes": 41,
|
||||
"rss_kb": 901240,
|
||||
"pss_kb": 857412,
|
||||
"private_dirty_kb": 840808,
|
||||
"vmhwm_kb": 901240,
|
||||
"utime_ticks": 959,
|
||||
"stime_ticks": 46,
|
||||
"cg_current": 3172499456,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 10088,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 81674,
|
||||
"pty_writes": 41,
|
||||
"rss_kb": 901584,
|
||||
"pss_kb": 857756,
|
||||
"private_dirty_kb": 841152,
|
||||
"vmhwm_kb": 901584,
|
||||
"utime_ticks": 977,
|
||||
"stime_ticks": 46,
|
||||
"cg_current": 3173011456,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 201
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "died",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 7,
|
||||
"signal": 0,
|
||||
"t": 10302
|
||||
},
|
||||
"stream_done": false,
|
||||
"msgs_streamed": 2901,
|
||||
"events_streamed": null,
|
||||
"pty_bytes_total": 83955,
|
||||
"pty_data_callbacks": 44,
|
||||
"first_byte_ms": 149,
|
||||
"session_create_ms": 201,
|
||||
"stream_start_ms": 1691,
|
||||
"vmhwm_kb": 901584,
|
||||
"cg_peak": 6536753152,
|
||||
"drain_max_loop_lag_ms": 10,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": null
|
||||
},
|
||||
"pty_tail": " lines) ◦ edit_fileOfficia fugia consequat minim. ·0.7s (7 lines)const x0 = 43function f3() { return x}⧉ copy⚕ ▼ Thinking: Irure nostrud tempor│Culpa cillum commodo enim adipiscing deserunt nulla duis veniam sed anim. Laboris magna amet culpa cillum │ commodo enim adipiscing deserunt nulla duis veniam. Irure nostrud tempor ipsum cupidatat voluptate.- Labore sit sunt esse aliquip.- Officia fugiat consequat minim elit mollit pariatur.- Aute quis eiusmod lorem.Occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna amet culpa cillum commodo enim. Quis eiusmod lorem occaecat reprehenderit exercitation.$terminal Irure nostrud tempor ipsum. · 0.5s (2 lines)◇rad_fie Labore it sunt esse. · 0.6s (18lines) ◦editOfficia fugiat conquatminim. ·0.7s (7 lines)⚡grep Autequis eiusmod lorem. · 0s ⧉ copy ⚕ ⧉ copy ⚕▼Thinking: Elit mollit pariatur││ ⧉ copy ⚕- - - const x0 = 6function f1() { return x} ⧉ copy ⚕◐Thought: Elit mollit pariatur - - - const x1 = 7function f2() { return x}⧉ copy●7 ⧉ copy ⚕ ● web_search Quis eiusmod lorem occaecat. · 0.9s (18 lines) ◆write_file Dolor proident velit laboris. · 1.0s (7 lines) ⚡terminal Cillum commodo enim adipiscing. · 0s◐- - - const x5 = 67function f2() { return x}Quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna amet. Mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris.- Dolor proident velit laboris magna.- Cillum commodo enim adipiscing deserunt nulla duis.- Veniam sed anim excepteur.Irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip. Sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt.$ terminal1.1s (2 lines)⚕ ⧉ copy ⚕◐Thought: Exercitation incididunt dolor ⧉ copy ⚕▍ ⚕◐Thought: Amet culpa cillum ⧉ copy ⚕ ◦edit_file Nostrud tempor ipsum cupidatat. · 0.1s (7 lines)8 - - - const x5 = 98function f3() { return x}⧉ copy⚕Nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip. Anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore sit sunt. Duis veniam sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore.- Sit sunt esse aliquip aliqua.- Fugiat consequat minim elit mollit pariatur aute.- Quis eiusmod lorem occaecat.Reprehenderit exercitation incididunt dolor proident velit laboris magna amet culpa. Eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit laboris magna.◦edit_file Nostrud tempor ipsum cupidatat. · 0.1s (7 lines)⚡grep Sitsunt esse aliquip. ·0s ⚕ ⧉ copy ⚕▼Thinking: Reprehenderit exercitation incididunt │ │ ⚕- - - const x0 = 63function f3() { return x} ⧉ copy⚕▼Thinking: Reprehenderit exercitation incididunt│Officia fugiat consequat minim elit mollit pariatur aute. Aliquip aliqua consectetur officia fugiat consequat│minim elit mollit. 9file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915\n throw new Error(`Failed to create optimized buffer: ${width}x${height}`);\n ^\nError: Failed to create optimized buffer: 120x12\n at FFIRenderLib.createOptimizedBuffer (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915:13)\n at OptimizedBuffer.create (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:11918:24)\n at TerminalConsole.show (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:21058:44)\n at CliRenderer.<anonymous> (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:23222:20)\n at process.emit (node:events:509:20)\n at process._fatalException (node:internal/process/execution:190:32)\nNode.js v26.3.0"
|
||||
}
|
||||
1336
bench/results/2026-06-10T2059-50e3471-mem3000-ink-ink-r0.json
Normal file
1395
bench/results/2026-06-10T2059-50e3471-mem3000-ink-ink-r1.json
Normal file
@@ -0,0 +1,782 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "mem3000",
|
||||
"ui": "opentui",
|
||||
"config": "otui-capped",
|
||||
"mode": "mem",
|
||||
"rep": 2,
|
||||
"run_id": "mq8jyv88-jxa2",
|
||||
"utc": "2026-06-10T21:00:19.736Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": "2G",
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": 3000,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-3000.ndjson",
|
||||
"msgs": 3000,
|
||||
"sha256": "0df05a04a611dda68aa07865f21c45b08edc78e0a71d4c8cb2b674729778d96d"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2369887,
|
||||
"gw_pid": 2369896,
|
||||
"cgroup": "/sys/fs/cgroup/user.slice/user-1001.slice/session-7349.scope",
|
||||
"load_avg_at_start": [
|
||||
1.06,
|
||||
0.64,
|
||||
0.49
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 27,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 44476,
|
||||
"pss_kb": 19055,
|
||||
"private_dirty_kb": 8312,
|
||||
"vmhwm_kb": 45344,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 0,
|
||||
"cg_current": 3178012672,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1034,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 12,
|
||||
"rss_kb": 104504,
|
||||
"pss_kb": 63459,
|
||||
"private_dirty_kb": 47832,
|
||||
"vmhwm_kb": 107224,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 3179597824,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1689,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 108328,
|
||||
"pss_kb": 67246,
|
||||
"private_dirty_kb": 51656,
|
||||
"vmhwm_kb": 108332,
|
||||
"utime_ticks": 18,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3185025024,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1714,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 120484,
|
||||
"pss_kb": 79217,
|
||||
"private_dirty_kb": 63496,
|
||||
"vmhwm_kb": 120492,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3186782208,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1715,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 120512,
|
||||
"pss_kb": 79245,
|
||||
"private_dirty_kb": 63524,
|
||||
"vmhwm_kb": 120520,
|
||||
"utime_ticks": 23,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3186782208,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1716,
|
||||
"msgs": 400,
|
||||
"events": 1432,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 120520,
|
||||
"pss_kb": 79253,
|
||||
"private_dirty_kb": 63532,
|
||||
"vmhwm_kb": 120520,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 3,
|
||||
"cg_current": 3186782208,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1990,
|
||||
"msgs": 501,
|
||||
"events": 1792,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 221328,
|
||||
"pss_kb": 177861,
|
||||
"private_dirty_kb": 161396,
|
||||
"vmhwm_kb": 221404,
|
||||
"utime_ticks": 74,
|
||||
"stime_ticks": 6,
|
||||
"cg_current": 3179270144,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2041,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 232640,
|
||||
"pss_kb": 189143,
|
||||
"private_dirty_kb": 172648,
|
||||
"vmhwm_kb": 232648,
|
||||
"utime_ticks": 80,
|
||||
"stime_ticks": 7,
|
||||
"cg_current": 3179270144,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2266,
|
||||
"msgs": 601,
|
||||
"events": 2154,
|
||||
"pty_bytes": 35690,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 265936,
|
||||
"pss_kb": 222429,
|
||||
"private_dirty_kb": 205944,
|
||||
"vmhwm_kb": 265936,
|
||||
"utime_ticks": 108,
|
||||
"stime_ticks": 9,
|
||||
"cg_current": 3176169472,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2527,
|
||||
"msgs": 701,
|
||||
"events": 2496,
|
||||
"pty_bytes": 40354,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 304772,
|
||||
"pss_kb": 261057,
|
||||
"private_dirty_kb": 244464,
|
||||
"vmhwm_kb": 305004,
|
||||
"utime_ticks": 143,
|
||||
"stime_ticks": 11,
|
||||
"cg_current": 3176157184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2531,
|
||||
"msgs": 801,
|
||||
"events": 2857,
|
||||
"pty_bytes": 40354,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 305492,
|
||||
"pss_kb": 261777,
|
||||
"private_dirty_kb": 245184,
|
||||
"vmhwm_kb": 305504,
|
||||
"utime_ticks": 144,
|
||||
"stime_ticks": 11,
|
||||
"cg_current": 3176157184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2533,
|
||||
"msgs": 901,
|
||||
"events": 3245,
|
||||
"pty_bytes": 40354,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 306248,
|
||||
"pss_kb": 262533,
|
||||
"private_dirty_kb": 245940,
|
||||
"vmhwm_kb": 306616,
|
||||
"utime_ticks": 145,
|
||||
"stime_ticks": 11,
|
||||
"cg_current": 3176157184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2535,
|
||||
"msgs": 1001,
|
||||
"events": 3588,
|
||||
"pty_bytes": 40354,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 308884,
|
||||
"pss_kb": 265169,
|
||||
"private_dirty_kb": 248576,
|
||||
"vmhwm_kb": 308916,
|
||||
"utime_ticks": 146,
|
||||
"stime_ticks": 11,
|
||||
"cg_current": 3176157184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2537,
|
||||
"msgs": 1101,
|
||||
"events": 3928,
|
||||
"pty_bytes": 40354,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 309176,
|
||||
"pss_kb": 265461,
|
||||
"private_dirty_kb": 248868,
|
||||
"vmhwm_kb": 309192,
|
||||
"utime_ticks": 146,
|
||||
"stime_ticks": 11,
|
||||
"cg_current": 3176157184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2539,
|
||||
"msgs": 1201,
|
||||
"events": 4298,
|
||||
"pty_bytes": 40354,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 309444,
|
||||
"pss_kb": 265729,
|
||||
"private_dirty_kb": 249136,
|
||||
"vmhwm_kb": 309492,
|
||||
"utime_ticks": 147,
|
||||
"stime_ticks": 11,
|
||||
"cg_current": 3176157184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3050,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 40354,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 460952,
|
||||
"pss_kb": 417124,
|
||||
"private_dirty_kb": 400520,
|
||||
"vmhwm_kb": 460952,
|
||||
"utime_ticks": 231,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3176210432,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3126,
|
||||
"msgs": 1300,
|
||||
"events": 4659,
|
||||
"pty_bytes": 43401,
|
||||
"pty_writes": 20,
|
||||
"rss_kb": 463816,
|
||||
"pss_kb": 419988,
|
||||
"private_dirty_kb": 403384,
|
||||
"vmhwm_kb": 463832,
|
||||
"utime_ticks": 239,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3175964672,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3427,
|
||||
"msgs": 1400,
|
||||
"events": 5011,
|
||||
"pty_bytes": 46365,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 493888,
|
||||
"pss_kb": 450060,
|
||||
"private_dirty_kb": 433456,
|
||||
"vmhwm_kb": 493964,
|
||||
"utime_ticks": 273,
|
||||
"stime_ticks": 22,
|
||||
"cg_current": 3175645184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3901,
|
||||
"msgs": 1500,
|
||||
"events": 5384,
|
||||
"pty_bytes": 51265,
|
||||
"pty_writes": 23,
|
||||
"rss_kb": 525952,
|
||||
"pss_kb": 482124,
|
||||
"private_dirty_kb": 465520,
|
||||
"vmhwm_kb": 525952,
|
||||
"utime_ticks": 323,
|
||||
"stime_ticks": 24,
|
||||
"cg_current": 3176116224,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4057,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 51265,
|
||||
"pty_writes": 23,
|
||||
"rss_kb": 526368,
|
||||
"pss_kb": 482540,
|
||||
"private_dirty_kb": 465936,
|
||||
"vmhwm_kb": 526368,
|
||||
"utime_ticks": 339,
|
||||
"stime_ticks": 24,
|
||||
"cg_current": 3176116224,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4430,
|
||||
"msgs": 1600,
|
||||
"events": 5730,
|
||||
"pty_bytes": 53320,
|
||||
"pty_writes": 25,
|
||||
"rss_kb": 557660,
|
||||
"pss_kb": 513832,
|
||||
"private_dirty_kb": 497228,
|
||||
"vmhwm_kb": 557660,
|
||||
"utime_ticks": 382,
|
||||
"stime_ticks": 25,
|
||||
"cg_current": 3175940096,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5006,
|
||||
"msgs": 1700,
|
||||
"events": 6100,
|
||||
"pty_bytes": 57903,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 578676,
|
||||
"pss_kb": 534848,
|
||||
"private_dirty_kb": 518244,
|
||||
"vmhwm_kb": 578704,
|
||||
"utime_ticks": 465,
|
||||
"stime_ticks": 27,
|
||||
"cg_current": 3175710720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5009,
|
||||
"msgs": 1800,
|
||||
"events": 6455,
|
||||
"pty_bytes": 57903,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 578944,
|
||||
"pss_kb": 535116,
|
||||
"private_dirty_kb": 518512,
|
||||
"vmhwm_kb": 579020,
|
||||
"utime_ticks": 465,
|
||||
"stime_ticks": 27,
|
||||
"cg_current": 3175710720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5012,
|
||||
"msgs": 1900,
|
||||
"events": 6838,
|
||||
"pty_bytes": 57903,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 579192,
|
||||
"pss_kb": 535364,
|
||||
"private_dirty_kb": 518760,
|
||||
"vmhwm_kb": 579280,
|
||||
"utime_ticks": 465,
|
||||
"stime_ticks": 27,
|
||||
"cg_current": 3175710720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5015,
|
||||
"msgs": 2000,
|
||||
"events": 7151,
|
||||
"pty_bytes": 57903,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 579412,
|
||||
"pss_kb": 535584,
|
||||
"private_dirty_kb": 518980,
|
||||
"vmhwm_kb": 579512,
|
||||
"utime_ticks": 465,
|
||||
"stime_ticks": 27,
|
||||
"cg_current": 3175710720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5017,
|
||||
"msgs": 2100,
|
||||
"events": 7532,
|
||||
"pty_bytes": 57903,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 579704,
|
||||
"pss_kb": 535876,
|
||||
"private_dirty_kb": 519272,
|
||||
"vmhwm_kb": 579748,
|
||||
"utime_ticks": 466,
|
||||
"stime_ticks": 27,
|
||||
"cg_current": 3175710720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5020,
|
||||
"msgs": 2201,
|
||||
"events": 7892,
|
||||
"pty_bytes": 57903,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 579840,
|
||||
"pss_kb": 536012,
|
||||
"private_dirty_kb": 519408,
|
||||
"vmhwm_kb": 579892,
|
||||
"utime_ticks": 466,
|
||||
"stime_ticks": 27,
|
||||
"cg_current": 3175710720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5023,
|
||||
"msgs": 2301,
|
||||
"events": 8254,
|
||||
"pty_bytes": 57903,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 580160,
|
||||
"pss_kb": 536332,
|
||||
"private_dirty_kb": 519728,
|
||||
"vmhwm_kb": 580244,
|
||||
"utime_ticks": 467,
|
||||
"stime_ticks": 27,
|
||||
"cg_current": 3175710720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 5056,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 57903,
|
||||
"pty_writes": 28,
|
||||
"rss_kb": 590484,
|
||||
"pss_kb": 546656,
|
||||
"private_dirty_kb": 530052,
|
||||
"vmhwm_kb": 590492,
|
||||
"utime_ticks": 471,
|
||||
"stime_ticks": 27,
|
||||
"cg_current": 3175710720,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5735,
|
||||
"msgs": 2401,
|
||||
"events": 8596,
|
||||
"pty_bytes": 60575,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 733132,
|
||||
"pss_kb": 689304,
|
||||
"private_dirty_kb": 672700,
|
||||
"vmhwm_kb": 733140,
|
||||
"utime_ticks": 550,
|
||||
"stime_ticks": 33,
|
||||
"cg_current": 3175919616,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 6059,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 60575,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 765664,
|
||||
"pss_kb": 721836,
|
||||
"private_dirty_kb": 705232,
|
||||
"vmhwm_kb": 765664,
|
||||
"utime_ticks": 584,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 3176443904,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 6460,
|
||||
"msgs": 2501,
|
||||
"events": 8957,
|
||||
"pty_bytes": 63490,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 769720,
|
||||
"pss_kb": 725892,
|
||||
"private_dirty_kb": 709288,
|
||||
"vmhwm_kb": 769736,
|
||||
"utime_ticks": 624,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 3176296448,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 7064,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 65023,
|
||||
"pty_writes": 32,
|
||||
"rss_kb": 797028,
|
||||
"pss_kb": 753200,
|
||||
"private_dirty_kb": 736596,
|
||||
"vmhwm_kb": 797028,
|
||||
"utime_ticks": 686,
|
||||
"stime_ticks": 38,
|
||||
"cg_current": 3176087552,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7241,
|
||||
"msgs": 2601,
|
||||
"events": 9345,
|
||||
"pty_bytes": 66694,
|
||||
"pty_writes": 33,
|
||||
"rss_kb": 798308,
|
||||
"pss_kb": 754480,
|
||||
"private_dirty_kb": 737876,
|
||||
"vmhwm_kb": 798316,
|
||||
"utime_ticks": 704,
|
||||
"stime_ticks": 38,
|
||||
"cg_current": 3175837696,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7798,
|
||||
"msgs": 2701,
|
||||
"events": 9688,
|
||||
"pty_bytes": 68453,
|
||||
"pty_writes": 34,
|
||||
"rss_kb": 825628,
|
||||
"pss_kb": 781800,
|
||||
"private_dirty_kb": 765196,
|
||||
"vmhwm_kb": 825628,
|
||||
"utime_ticks": 761,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 3176284160,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 8076,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 69629,
|
||||
"pty_writes": 35,
|
||||
"rss_kb": 831440,
|
||||
"pss_kb": 787612,
|
||||
"private_dirty_kb": 771008,
|
||||
"vmhwm_kb": 834344,
|
||||
"utime_ticks": 789,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 3176284160,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 8654,
|
||||
"msgs": 2801,
|
||||
"events": 10028,
|
||||
"pty_bytes": 71439,
|
||||
"pty_writes": 36,
|
||||
"rss_kb": 856908,
|
||||
"pss_kb": 813080,
|
||||
"private_dirty_kb": 796476,
|
||||
"vmhwm_kb": 856908,
|
||||
"utime_ticks": 847,
|
||||
"stime_ticks": 42,
|
||||
"cg_current": 3176873984,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 9080,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 74354,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 885440,
|
||||
"pss_kb": 841612,
|
||||
"private_dirty_kb": 825008,
|
||||
"vmhwm_kb": 885440,
|
||||
"utime_ticks": 890,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 3177000960,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9505,
|
||||
"msgs": 2901,
|
||||
"events": 10398,
|
||||
"pty_bytes": 76085,
|
||||
"pty_writes": 38,
|
||||
"rss_kb": 888096,
|
||||
"pss_kb": 844268,
|
||||
"private_dirty_kb": 827664,
|
||||
"vmhwm_kb": 888096,
|
||||
"utime_ticks": 934,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 3176677376,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9807,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 76940,
|
||||
"pty_writes": 39,
|
||||
"rss_kb": 891124,
|
||||
"pss_kb": 847296,
|
||||
"private_dirty_kb": 830692,
|
||||
"vmhwm_kb": 891164,
|
||||
"utime_ticks": 965,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 3176747008,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 9814,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 76940,
|
||||
"pty_writes": 39,
|
||||
"rss_kb": 891240,
|
||||
"pss_kb": 847412,
|
||||
"private_dirty_kb": 830808,
|
||||
"vmhwm_kb": 891248,
|
||||
"utime_ticks": 965,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 3176747008,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 177
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 177
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 177
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "crashed_after_stream",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 7,
|
||||
"signal": 0,
|
||||
"t": 9862
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 3000,
|
||||
"events_streamed": 10759,
|
||||
"pty_bytes_total": 78203,
|
||||
"pty_data_callbacks": 40,
|
||||
"first_byte_ms": 141,
|
||||
"session_create_ms": 177,
|
||||
"stream_start_ms": 1688,
|
||||
"vmhwm_kb": 891248,
|
||||
"cg_peak": 6536753152,
|
||||
"drain_max_loop_lag_ms": 22,
|
||||
"drain_lag_violations": 3,
|
||||
"drain_ok": false,
|
||||
"digest": null
|
||||
},
|
||||
"pty_tail": " commodo enim adipiscing.- Duis veniam sed anim excepteur irure nostrud.- Tempor ipsum cupidatat voluptate.Ullamco labore sit sunt esse aliquip aliqua consectetur. Ipsum cupidatat voluptate ullamco labore sit sunt esse aliquip.⚡terminal Incididunt dolor proident velit. · 0s◐Thought: Proident velit laboris ⧉ copy ⚕ ◦ edit_file Sed anim excepteu rure. · 13s (7 lines) - - - const x2 = 90function f0() { return x}⧉ copy⚕Sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate ullamco labore. Nulla duis veniam sed anim excepteur irure nostrud tempor ipsum cupidatat voluptate. Enim adipiscing deserunt nulla duis veniam sed anim excepteur irure nostrud tempor ipsum.- Cupidatat voluptate ullamco labore sit.- Aliquip aliqua consectetur officia fugiat consequat minim.- Elit mollit pariatur aute.Quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor proident velit. Mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation incididunt dolor.◦edit_file Sed anim excepteur irure. · 1.3s (7 lines)⚡grep Cupidatat voluptate ullamco labore. ·0s 6 ⧉ copy⚕ ◦edit_file Nulla duis veniam sed. · 3.3s (7 lines) ◦ grep Nostrud tempor ipsum cupidatat. · 3.4s (2 lines) ●web_search Sit sunt esse aliquip. · 3.5s (18 lines) - const x0 = 51function f1() { return x}⧉ copy⚕ ⧉ copy◦edit_file Nulla duis veniam sed. · 3.3s (7 lines) ◦grep Nostrud tempor ipsum cupidatat. · 3.4s (2 lines) ●web_search Sit sunt esse aliquip. · 3.5s (18 lines) ⧉ copy● ⧉ copy ⚕ ▼ Thinking: Mollit pariatur aute ││ $terminal Mollit pariatur aute quis. · 1.7s (2 lines) ◇ read_file Reprehenderit exercitation incididunt dolor. · 1.8s (18 lines)Magnametculp cillum. · 1.9s (7 lines)Deent nulladuis veniam. · 2.0s (2lines) Irure ostrudtempor ipsum. ·2.s (18 lines)◆write_file Labore sit sunt esse. · 2.2s (7 lines) $ terminal Officia fugiat consequat minim. · 2.3s (2 lines)◐7⚕▼Thinking: Mollit pariatur aute │ Labore sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur. │Cupidatat voluptate ullamco labore sit sunt. Mollit pariatur aute quis eiusmod lorem occaecat reprehenderit exercitation. - Reprehenderit exercitation incididunt dolor proident.- Magna amet culpa cillum commodo enim adipiscing.- Deserunt nulla duis veniam.Sed anim excepteur irure nostrud tempor ipsum cupidatat. Nulla duis veniam sed anim excepteur irure nostrud tempor.$terminal Mollit pariatur aute quis. · 1.7s (2 lines)◇read_file Reprehenderit exercitation incididunt dolor. · 1.8s (18 lines) ◦edit_file Magna amet culpa cillum. · 1.9s (7 lines) ◦grep Deserunt nulla duis veniam. · 2.0s (2 lines) ●web_search Irure nostrud tempor ipsum. · 2.1s (18 lines) ◆write_file Labore sit sunt esse. · 2.2s (7 lines) $terminal Officia fugiat consequat minim. · 2.3s (2 lines) ⧉ copy ⚕ ⧉ copy ⚕ ⧉ copy ⚕ - - - const x4 = 74function f4() { return x}⧉ copy ⚕ - - - const x5 = 75function f0() { return x}⧉ copy●9file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915\n throw new Error(`Failed to create optimized buffer: ${width}x${height}`);\n ^\nError: Failed to create optimized buffer: 120x12\n at FFIRenderLib.createOptimizedBuffer (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915:13)\n at OptimizedBuffer.create (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:11918:24)\n at TerminalConsole.show (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:21058:44)\n at CliRenderer.<anonymous> (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:23222:20)\n at process.emit (node:events:509:20)\n at process._fatalException (node:internal/process/execution:190:32)\nNode.js v26.3.0"
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "mem3000",
|
||||
"ui": "opentui",
|
||||
"config": "otui-uncapped",
|
||||
"mode": "mem",
|
||||
"rep": 2,
|
||||
"run_id": "mq8jzaoz-onls",
|
||||
"utc": "2026-06-10T21:00:39.779Z",
|
||||
"sha": "50e34713b",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": "2G",
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": 100000,
|
||||
"fixture": {
|
||||
"path": "/home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/bench/.cache/fixture-3000.ndjson",
|
||||
"msgs": 3000,
|
||||
"sha256": "0df05a04a611dda68aa07865f21c45b08edc78e0a71d4c8cb2b674729778d96d"
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2370298,
|
||||
"gw_pid": 2370307,
|
||||
"cgroup": "/sys/fs/cgroup/user.slice/user-1001.slice/session-7349.scope",
|
||||
"load_avg_at_start": [
|
||||
0.91,
|
||||
0.63,
|
||||
0.49
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 27,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 33220,
|
||||
"pss_kb": 14193,
|
||||
"private_dirty_kb": 6396,
|
||||
"vmhwm_kb": 33228,
|
||||
"utime_ticks": 0,
|
||||
"stime_ticks": 0,
|
||||
"cg_current": 3181150208,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1032,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 10,
|
||||
"rss_kb": 104900,
|
||||
"pss_kb": 63836,
|
||||
"private_dirty_kb": 48180,
|
||||
"vmhwm_kb": 107872,
|
||||
"utime_ticks": 18,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 3182235648,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1689,
|
||||
"msgs": 100,
|
||||
"events": 355,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 107280,
|
||||
"pss_kb": 66195,
|
||||
"private_dirty_kb": 50560,
|
||||
"vmhwm_kb": 107872,
|
||||
"utime_ticks": 19,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 3185168384,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1714,
|
||||
"msgs": 200,
|
||||
"events": 738,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 119640,
|
||||
"pss_kb": 78475,
|
||||
"private_dirty_kb": 62792,
|
||||
"vmhwm_kb": 119756,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 3183779840,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1716,
|
||||
"msgs": 300,
|
||||
"events": 1051,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 119912,
|
||||
"pss_kb": 78747,
|
||||
"private_dirty_kb": 63064,
|
||||
"vmhwm_kb": 119944,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 3183779840,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 1717,
|
||||
"msgs": 400,
|
||||
"events": 1432,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 119992,
|
||||
"pss_kb": 78827,
|
||||
"private_dirty_kb": 63144,
|
||||
"vmhwm_kb": 120252,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 2,
|
||||
"cg_current": 3183525888,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 2041,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 31413,
|
||||
"pty_writes": 13,
|
||||
"rss_kb": 230864,
|
||||
"pss_kb": 187378,
|
||||
"private_dirty_kb": 170884,
|
||||
"vmhwm_kb": 230864,
|
||||
"utime_ticks": 81,
|
||||
"stime_ticks": 6,
|
||||
"cg_current": 3186503680,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2140,
|
||||
"msgs": 501,
|
||||
"events": 1792,
|
||||
"pty_bytes": 33737,
|
||||
"pty_writes": 15,
|
||||
"rss_kb": 233388,
|
||||
"pss_kb": 189764,
|
||||
"private_dirty_kb": 173220,
|
||||
"vmhwm_kb": 233388,
|
||||
"utime_ticks": 93,
|
||||
"stime_ticks": 7,
|
||||
"cg_current": 3185446912,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2317,
|
||||
"msgs": 601,
|
||||
"events": 2154,
|
||||
"pty_bytes": 35582,
|
||||
"pty_writes": 16,
|
||||
"rss_kb": 274096,
|
||||
"pss_kb": 230408,
|
||||
"private_dirty_kb": 213800,
|
||||
"vmhwm_kb": 274152,
|
||||
"utime_ticks": 120,
|
||||
"stime_ticks": 8,
|
||||
"cg_current": 3185287168,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2542,
|
||||
"msgs": 701,
|
||||
"events": 2496,
|
||||
"pty_bytes": 41569,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 319456,
|
||||
"pss_kb": 275645,
|
||||
"private_dirty_kb": 259036,
|
||||
"vmhwm_kb": 336796,
|
||||
"utime_ticks": 166,
|
||||
"stime_ticks": 13,
|
||||
"cg_current": 3184562176,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 2793,
|
||||
"msgs": 801,
|
||||
"events": 2857,
|
||||
"pty_bytes": 46542,
|
||||
"pty_writes": 21,
|
||||
"rss_kb": 350128,
|
||||
"pss_kb": 306317,
|
||||
"private_dirty_kb": 289708,
|
||||
"vmhwm_kb": 350128,
|
||||
"utime_ticks": 195,
|
||||
"stime_ticks": 14,
|
||||
"cg_current": 3184861184,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 3044,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 47578,
|
||||
"pty_writes": 22,
|
||||
"rss_kb": 383596,
|
||||
"pss_kb": 339785,
|
||||
"private_dirty_kb": 323176,
|
||||
"vmhwm_kb": 383596,
|
||||
"utime_ticks": 224,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3184898048,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3095,
|
||||
"msgs": 901,
|
||||
"events": 3245,
|
||||
"pty_bytes": 48697,
|
||||
"pty_writes": 23,
|
||||
"rss_kb": 384708,
|
||||
"pss_kb": 340892,
|
||||
"private_dirty_kb": 324288,
|
||||
"vmhwm_kb": 384708,
|
||||
"utime_ticks": 229,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3184648192,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3195,
|
||||
"msgs": 1001,
|
||||
"events": 3588,
|
||||
"pty_bytes": 50416,
|
||||
"pty_writes": 24,
|
||||
"rss_kb": 387812,
|
||||
"pss_kb": 343996,
|
||||
"private_dirty_kb": 327392,
|
||||
"vmhwm_kb": 387840,
|
||||
"utime_ticks": 241,
|
||||
"stime_ticks": 15,
|
||||
"cg_current": 3184648192,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3524,
|
||||
"msgs": 1101,
|
||||
"events": 3928,
|
||||
"pty_bytes": 54393,
|
||||
"pty_writes": 27,
|
||||
"rss_kb": 416784,
|
||||
"pss_kb": 372968,
|
||||
"private_dirty_kb": 356364,
|
||||
"vmhwm_kb": 417092,
|
||||
"utime_ticks": 276,
|
||||
"stime_ticks": 17,
|
||||
"cg_current": 3185565696,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 3925,
|
||||
"msgs": 1201,
|
||||
"events": 4298,
|
||||
"pty_bytes": 57944,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 449756,
|
||||
"pss_kb": 405940,
|
||||
"private_dirty_kb": 389336,
|
||||
"vmhwm_kb": 449776,
|
||||
"utime_ticks": 321,
|
||||
"stime_ticks": 19,
|
||||
"cg_current": 3184865280,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 4050,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 57944,
|
||||
"pty_writes": 29,
|
||||
"rss_kb": 472700,
|
||||
"pss_kb": 428884,
|
||||
"private_dirty_kb": 412280,
|
||||
"vmhwm_kb": 472764,
|
||||
"utime_ticks": 335,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184816128,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4199,
|
||||
"msgs": 1300,
|
||||
"events": 4659,
|
||||
"pty_bytes": 59534,
|
||||
"pty_writes": 30,
|
||||
"rss_kb": 477008,
|
||||
"pss_kb": 433192,
|
||||
"private_dirty_kb": 416588,
|
||||
"vmhwm_kb": 477008,
|
||||
"utime_ticks": 350,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184738304,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4227,
|
||||
"msgs": 1400,
|
||||
"events": 5011,
|
||||
"pty_bytes": 60792,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 478412,
|
||||
"pss_kb": 434596,
|
||||
"private_dirty_kb": 417992,
|
||||
"vmhwm_kb": 478460,
|
||||
"utime_ticks": 355,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184717824,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4231,
|
||||
"msgs": 1500,
|
||||
"events": 5384,
|
||||
"pty_bytes": 60792,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 478732,
|
||||
"pss_kb": 434916,
|
||||
"private_dirty_kb": 418312,
|
||||
"vmhwm_kb": 479492,
|
||||
"utime_ticks": 356,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184459776,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4234,
|
||||
"msgs": 1600,
|
||||
"events": 5730,
|
||||
"pty_bytes": 60792,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 479576,
|
||||
"pss_kb": 435760,
|
||||
"private_dirty_kb": 419156,
|
||||
"vmhwm_kb": 479668,
|
||||
"utime_ticks": 356,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184439296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4237,
|
||||
"msgs": 1700,
|
||||
"events": 6100,
|
||||
"pty_bytes": 60792,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 479844,
|
||||
"pss_kb": 436028,
|
||||
"private_dirty_kb": 419424,
|
||||
"vmhwm_kb": 479952,
|
||||
"utime_ticks": 356,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184439296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4240,
|
||||
"msgs": 1800,
|
||||
"events": 6455,
|
||||
"pty_bytes": 60792,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 480112,
|
||||
"pss_kb": 436296,
|
||||
"private_dirty_kb": 419692,
|
||||
"vmhwm_kb": 480184,
|
||||
"utime_ticks": 356,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184439296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4242,
|
||||
"msgs": 1900,
|
||||
"events": 6838,
|
||||
"pty_bytes": 60792,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 480300,
|
||||
"pss_kb": 436484,
|
||||
"private_dirty_kb": 419880,
|
||||
"vmhwm_kb": 480364,
|
||||
"utime_ticks": 357,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184439296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 4244,
|
||||
"msgs": 2000,
|
||||
"events": 7151,
|
||||
"pty_bytes": 60792,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 480524,
|
||||
"pss_kb": 436708,
|
||||
"private_dirty_kb": 420104,
|
||||
"vmhwm_kb": 480576,
|
||||
"utime_ticks": 357,
|
||||
"stime_ticks": 20,
|
||||
"cg_current": 3184439296,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 5055,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 60792,
|
||||
"pty_writes": 31,
|
||||
"rss_kb": 664920,
|
||||
"pss_kb": 621104,
|
||||
"private_dirty_kb": 604500,
|
||||
"vmhwm_kb": 664944,
|
||||
"utime_ticks": 473,
|
||||
"stime_ticks": 26,
|
||||
"cg_current": 3184832512,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5383,
|
||||
"msgs": 2100,
|
||||
"events": 7532,
|
||||
"pty_bytes": 64936,
|
||||
"pty_writes": 33,
|
||||
"rss_kb": 669004,
|
||||
"pss_kb": 625188,
|
||||
"private_dirty_kb": 608584,
|
||||
"vmhwm_kb": 669028,
|
||||
"utime_ticks": 506,
|
||||
"stime_ticks": 28,
|
||||
"cg_current": 3184775168,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 5809,
|
||||
"msgs": 2201,
|
||||
"events": 7892,
|
||||
"pty_bytes": 66459,
|
||||
"pty_writes": 34,
|
||||
"rss_kb": 698060,
|
||||
"pss_kb": 654244,
|
||||
"private_dirty_kb": 637640,
|
||||
"vmhwm_kb": 698060,
|
||||
"utime_ticks": 551,
|
||||
"stime_ticks": 30,
|
||||
"cg_current": 3184918528,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 6061,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 68410,
|
||||
"pty_writes": 35,
|
||||
"rss_kb": 715944,
|
||||
"pss_kb": 672128,
|
||||
"private_dirty_kb": 655524,
|
||||
"vmhwm_kb": 716072,
|
||||
"utime_ticks": 579,
|
||||
"stime_ticks": 31,
|
||||
"cg_current": 3184750592,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 6512,
|
||||
"msgs": 2301,
|
||||
"events": 8254,
|
||||
"pty_bytes": 70180,
|
||||
"pty_writes": 36,
|
||||
"rss_kb": 729140,
|
||||
"pss_kb": 685313,
|
||||
"private_dirty_kb": 668720,
|
||||
"vmhwm_kb": 729140,
|
||||
"utime_ticks": 623,
|
||||
"stime_ticks": 33,
|
||||
"cg_current": 3184795648,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 7069,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 71849,
|
||||
"pty_writes": 37,
|
||||
"rss_kb": 759640,
|
||||
"pss_kb": 715813,
|
||||
"private_dirty_kb": 699220,
|
||||
"vmhwm_kb": 759640,
|
||||
"utime_ticks": 682,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 3184627712,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7243,
|
||||
"msgs": 2401,
|
||||
"events": 8596,
|
||||
"pty_bytes": 73318,
|
||||
"pty_writes": 38,
|
||||
"rss_kb": 760072,
|
||||
"pss_kb": 716245,
|
||||
"private_dirty_kb": 699652,
|
||||
"vmhwm_kb": 760072,
|
||||
"utime_ticks": 700,
|
||||
"stime_ticks": 35,
|
||||
"cg_current": 3185369088,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 7974,
|
||||
"msgs": 2501,
|
||||
"events": 8957,
|
||||
"pty_bytes": 76046,
|
||||
"pty_writes": 40,
|
||||
"rss_kb": 790560,
|
||||
"pss_kb": 746733,
|
||||
"private_dirty_kb": 730140,
|
||||
"vmhwm_kb": 790560,
|
||||
"utime_ticks": 775,
|
||||
"stime_ticks": 38,
|
||||
"cg_current": 3185258496,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 8074,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 76046,
|
||||
"pty_writes": 40,
|
||||
"rss_kb": 790560,
|
||||
"pss_kb": 746733,
|
||||
"private_dirty_kb": 730140,
|
||||
"vmhwm_kb": 790560,
|
||||
"utime_ticks": 785,
|
||||
"stime_ticks": 38,
|
||||
"cg_current": 3185254400,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 8803,
|
||||
"msgs": 2601,
|
||||
"events": 9345,
|
||||
"pty_bytes": 80929,
|
||||
"pty_writes": 42,
|
||||
"rss_kb": 821056,
|
||||
"pss_kb": 777198,
|
||||
"private_dirty_kb": 760636,
|
||||
"vmhwm_kb": 821056,
|
||||
"utime_ticks": 860,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 3186028544,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9056,
|
||||
"msgs": 2701,
|
||||
"events": 9688,
|
||||
"pty_bytes": 82978,
|
||||
"pty_writes": 43,
|
||||
"rss_kb": 821896,
|
||||
"pss_kb": 778038,
|
||||
"private_dirty_kb": 761476,
|
||||
"vmhwm_kb": 821904,
|
||||
"utime_ticks": 885,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 3185565696,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 9079,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 82978,
|
||||
"pty_writes": 43,
|
||||
"rss_kb": 823668,
|
||||
"pss_kb": 779810,
|
||||
"private_dirty_kb": 763248,
|
||||
"vmhwm_kb": 823688,
|
||||
"utime_ticks": 887,
|
||||
"stime_ticks": 40,
|
||||
"cg_current": 3185565696,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 9909,
|
||||
"msgs": 2801,
|
||||
"events": 10028,
|
||||
"pty_bytes": 86325,
|
||||
"pty_writes": 45,
|
||||
"rss_kb": 852852,
|
||||
"pss_kb": 808994,
|
||||
"private_dirty_kb": 792432,
|
||||
"vmhwm_kb": 852868,
|
||||
"utime_ticks": 972,
|
||||
"stime_ticks": 42,
|
||||
"cg_current": 3186216960,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 10086,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 86325,
|
||||
"pty_writes": 45,
|
||||
"rss_kb": 878688,
|
||||
"pss_kb": 834830,
|
||||
"private_dirty_kb": 818268,
|
||||
"vmhwm_kb": 878920,
|
||||
"utime_ticks": 989,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 3186737152,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 10811,
|
||||
"msgs": 2901,
|
||||
"events": 10398,
|
||||
"pty_bytes": 91739,
|
||||
"pty_writes": 47,
|
||||
"rss_kb": 885784,
|
||||
"pss_kb": 841926,
|
||||
"private_dirty_kb": 825364,
|
||||
"vmhwm_kb": 885800,
|
||||
"utime_ticks": 1063,
|
||||
"stime_ticks": 45,
|
||||
"cg_current": 3188797440,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 11086,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 91739,
|
||||
"pty_writes": 47,
|
||||
"rss_kb": 911912,
|
||||
"pss_kb": 868054,
|
||||
"private_dirty_kb": 851492,
|
||||
"vmhwm_kb": 911912,
|
||||
"utime_ticks": 1091,
|
||||
"stime_ticks": 47,
|
||||
"cg_current": 3191078912,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "boundary",
|
||||
"t_ms": 11439,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 94867,
|
||||
"pty_writes": 48,
|
||||
"rss_kb": 913956,
|
||||
"pss_kb": 870098,
|
||||
"private_dirty_kb": 853536,
|
||||
"vmhwm_kb": 913948,
|
||||
"utime_ticks": 1125,
|
||||
"stime_ticks": 48,
|
||||
"cg_current": 3189575680,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
},
|
||||
{
|
||||
"kind": "done",
|
||||
"t_ms": 11446,
|
||||
"msgs": 3000,
|
||||
"events": 10759,
|
||||
"pty_bytes": 94867,
|
||||
"pty_writes": 48,
|
||||
"rss_kb": 913948,
|
||||
"pss_kb": 870090,
|
||||
"private_dirty_kb": 853528,
|
||||
"vmhwm_kb": 913948,
|
||||
"utime_ticks": 1125,
|
||||
"stime_ticks": 48,
|
||||
"cg_current": 3189575680,
|
||||
"cg_peak": 6536753152,
|
||||
"cg_oom_kill": 0
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 177
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 177
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 177
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "crashed_after_stream",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 7,
|
||||
"signal": 0,
|
||||
"t": 11857
|
||||
},
|
||||
"stream_done": true,
|
||||
"msgs_streamed": 3000,
|
||||
"events_streamed": 10759,
|
||||
"pty_bytes_total": 98672,
|
||||
"pty_data_callbacks": 51,
|
||||
"first_byte_ms": 142,
|
||||
"session_create_ms": 177,
|
||||
"stream_start_ms": 1687,
|
||||
"vmhwm_kb": 913948,
|
||||
"cg_peak": 6536753152,
|
||||
"drain_max_loop_lag_ms": 19,
|
||||
"drain_lag_violations": 2,
|
||||
"drain_ok": false,
|
||||
"digest": null
|
||||
},
|
||||
"pty_tail": "at consequat. Voluptate ullamco labore sit sunt esse aliquip aliqua consectetur officia. - Elit mollit pariatur aute quis.- Occaecat reprehenderit exercitation incididunt dolor proident velit.- Laboris magna amet culpa.Cillum commodo enim adipiscing deserunt nulla duis. Magna amet culpa cillum commodo enim adipiscing deserunt.◦edit_file Aliquip aliqua consectetur officia. · 2.9s (7 lines)⚡grep Elitmollit pratur aut. · 0s ⧉ copy ⚕ ◦edit_file Sit sunt esse aliquip. · 0.9s (7 lines) ◦ grep Fugiat consequat minim elit. · 1.0s (2 lines) ●web_search Quis eiusmod lorem occaecat. · 1.1s (18 lines)◆write_file Dolor proident velit laboris. · 1.2s (7 lines)$terminal Cillum commodo enim adipiscing. · 1.3s (2 lines) ◇ read_file Veniam sed anim excepteur. · 1.4s (18 lines) Ipsum cupidatt volupae ullamo. · 1.5s (7 lines) ◦ grepssealiquip lqua consctetur. · 1.6s (2 lines)⧉ copy⚕Sit sunt esse aliquip aliqua consectetur officia fugiat consequat minim elit mollit pariatur aute. Voluptate ullamco labore sit sunt esse. Tempor ipsum cupidatat voluptate ullamco labore sit. - Fugiat consequat minim elit mollit.- Quis eiusmod lorem occaecat reprehenderit exercitation incididunt.- Dolor proident velit laboris.Magna amet culpa cillum commodo enim adipiscing deserunt nulla duis veniam sed anim. Proident velit laboris magna amet culpa cillum commodo enim adipiscing deserunt nulla duis veniam.◦edit_file Sit sunt esse aliquip. · 0.9s (7 lines)grep Fugiat consequat minim elit. · 1.0s (2 lines)●web_search Quis ismod lorem occaecat ·1.1s (18 lines)◆rite_fileDolor proident vlit lboris. · 1.2s (7$terminal Cillum commoo enim adipiscing32◇rad_fie Veniam sed anim excepteur. · 1.4s (18lines) ◦editIpsum cupidtatvoluate ullamco.· 1.5s (7 lines)grep Essealiquip aliqu conseceur. · 1.6s (2 lines) ⚡web_search Mnim elit mollit pariatur. · 0s 10s │ …/lively-thrush/hermes-agent ⧉ copy ◦ edit_file Voluptate ullamco labore sit. · 2.9s (7 lines) ◦grep Aliqua consectetur officia fugiat. · 3.0s (2 lines) ●web_search Mollit pariatur aute quis. · 3.1s (18 lines) ◆write_file Reprehenderit exercitation incididunt dolor. · 3.2s (7 lines) $terminal Magna amet culpa cillum. · 3.3s (2 lines) ◇read_file Deserunt nulla duis veniam. · 3.4s (18 lines) ◦edit_file Irure nostrud tempor ipsum. · 3.5s (7 lines) ◦grep Labore sit sunt esse. · 3.6s (2 lines) ●web_search Officia fugiat consequat minim. · 3.7s (18 lines) ⧉ copy ●- - const x3 = 8function f3() { return x}⧉ copy ◦ edit_file Voluptate ullamco labore sit. · 2.9s (7 lines)grep Aliqua consectetur officia fugia302●web_search Mollit pariatur auteqis. ·3.1s (18 lines) ◆rite_fileReprehenderi exercitatonincididunt door. · 3.2s (7 lines)$terminal Magna amt culpa cillum. · 3.3s (2 lines) ◇rad_fie Deserunt nula dus veniam. · 3.4s (18 lines)◦editIrure ostrud tempor ipsu57 lines) grep Labore sitsun esse. · 3.6 (2lines) ●web_search Officia fugiat consequatminim. · 3.7s (18 lines) ⧉ copy⚕▍ ◐file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915\n throw new Error(`Failed to create optimized buffer: ${width}x${height}`);\n ^\nError: Failed to create optimized buffer: 120x12\n at FFIRenderLib.createOptimizedBuffer (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:14915:13)\n at OptimizedBuffer.create (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:11918:24)\n at TerminalConsole.show (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:21058:44)\n at CliRenderer.<anonymous> (file:///home/daimon/github/worktrees/hermes-agent/lively-thrush/hermes-agent/ui-opentui/node_modules/@opentui/core/index-59t85rvq.js:23222:20)\n at process.emit (node:events:509:20)\n at process._fatalException (node:internal/process/execution:190:32)\nNode.js v26.3.0"
|
||||
}
|
||||
1331
bench/results/2026-06-10T2101-197d499-mem3000-ink-ink-r2.json
Normal file
162
bench/results/2026-06-10T2103-197d499-startup-ink-ink-r0.json
Normal file
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "startup",
|
||||
"ui": "ink",
|
||||
"config": "ink",
|
||||
"mode": "startup",
|
||||
"rep": 0,
|
||||
"run_id": "mq8k2tye-1xyi",
|
||||
"utc": "2026-06-10T21:03:24.711Z",
|
||||
"sha": "197d49948",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": null,
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": null,
|
||||
"fixture": {
|
||||
"path": "",
|
||||
"msgs": 0,
|
||||
"sha256": ""
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2373306,
|
||||
"gw_pid": 2373314,
|
||||
"cgroup": null,
|
||||
"load_avg_at_start": [
|
||||
0.43,
|
||||
0.55,
|
||||
0.48
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 27,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 55524,
|
||||
"pss_kb": 29228,
|
||||
"private_dirty_kb": 18060,
|
||||
"vmhwm_kb": 55556,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 0
|
||||
},
|
||||
{
|
||||
"kind": "final",
|
||||
"t_ms": 933,
|
||||
"msgs": 0,
|
||||
"events": 0,
|
||||
"pty_bytes": 6604,
|
||||
"pty_writes": 10,
|
||||
"rss_kb": 108996,
|
||||
"pss_kb": 69637,
|
||||
"private_dirty_kb": 49764,
|
||||
"vmhwm_kb": 108996,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 3
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 176
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "commands.catalog",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "setup.status",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 201
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.active_list",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 227
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "completed",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 0,
|
||||
"signal": 0,
|
||||
"t": 943
|
||||
},
|
||||
"stream_done": false,
|
||||
"msgs_streamed": 0,
|
||||
"events_streamed": null,
|
||||
"pty_bytes_total": 6785,
|
||||
"pty_data_callbacks": 18,
|
||||
"first_byte_ms": 71,
|
||||
"session_create_ms": 227,
|
||||
"stream_start_ms": null,
|
||||
"vmhwm_kb": 108996,
|
||||
"cg_peak": null,
|
||||
"drain_max_loop_lag_ms": 1,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": null
|
||||
}
|
||||
}
|
||||
157
bench/results/2026-06-10T2103-197d499-startup-ink-ink-r1.json
Normal file
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "startup",
|
||||
"ui": "ink",
|
||||
"config": "ink",
|
||||
"mode": "startup",
|
||||
"rep": 1,
|
||||
"run_id": "mq8k3k0d-8lb5",
|
||||
"utc": "2026-06-10T21:03:58.478Z",
|
||||
"sha": "197d49948",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": null,
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": null,
|
||||
"fixture": {
|
||||
"path": "",
|
||||
"msgs": 0,
|
||||
"sha256": ""
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2374052,
|
||||
"gw_pid": 2374068,
|
||||
"cgroup": null,
|
||||
"load_avg_at_start": [
|
||||
0.24,
|
||||
0.49,
|
||||
0.46
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 26,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 56656,
|
||||
"pss_kb": 30387,
|
||||
"private_dirty_kb": 19220,
|
||||
"vmhwm_kb": 56664,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 0
|
||||
},
|
||||
{
|
||||
"kind": "final",
|
||||
"t_ms": 933,
|
||||
"msgs": 0,
|
||||
"events": 0,
|
||||
"pty_bytes": 6604,
|
||||
"pty_writes": 10,
|
||||
"rss_kb": 107996,
|
||||
"pss_kb": 68555,
|
||||
"private_dirty_kb": 48740,
|
||||
"vmhwm_kb": 107996,
|
||||
"utime_ticks": 24,
|
||||
"stime_ticks": 2
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 176
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "commands.catalog",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "setup.status",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 202
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.active_list",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 226
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "config.get",
|
||||
"t_ms": 226
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "completed",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 0,
|
||||
"signal": 0,
|
||||
"t": 945
|
||||
},
|
||||
"stream_done": false,
|
||||
"msgs_streamed": 0,
|
||||
"events_streamed": null,
|
||||
"pty_bytes_total": 6785,
|
||||
"pty_data_callbacks": 26,
|
||||
"first_byte_ms": 67,
|
||||
"session_create_ms": 202,
|
||||
"stream_start_ms": null,
|
||||
"vmhwm_kb": 107996,
|
||||
"cg_peak": null,
|
||||
"drain_max_loop_lag_ms": 2,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "startup",
|
||||
"ui": "opentui",
|
||||
"config": "otui-capped",
|
||||
"mode": "startup",
|
||||
"rep": 0,
|
||||
"run_id": "mq8k32ir-kfs0",
|
||||
"utc": "2026-06-10T21:03:35.811Z",
|
||||
"sha": "197d49948",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": null,
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": 3000,
|
||||
"fixture": {
|
||||
"path": "",
|
||||
"msgs": 0,
|
||||
"sha256": ""
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2373543,
|
||||
"gw_pid": 2373552,
|
||||
"cgroup": null,
|
||||
"load_avg_at_start": [
|
||||
0.37,
|
||||
0.54,
|
||||
0.47
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 26,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 54272,
|
||||
"pss_kb": 27947,
|
||||
"private_dirty_kb": 16840,
|
||||
"vmhwm_kb": 54312,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 0
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1032,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 104972,
|
||||
"pss_kb": 64818,
|
||||
"private_dirty_kb": 48264,
|
||||
"vmhwm_kb": 107336,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 2
|
||||
},
|
||||
{
|
||||
"kind": "final",
|
||||
"t_ms": 1139,
|
||||
"msgs": 0,
|
||||
"events": 0,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 19,
|
||||
"rss_kb": 104972,
|
||||
"pss_kb": 64818,
|
||||
"private_dirty_kb": 48264,
|
||||
"vmhwm_kb": 107336,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 2
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 176
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 176
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 176
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "completed",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 0,
|
||||
"signal": 0,
|
||||
"t": 1322
|
||||
},
|
||||
"stream_done": false,
|
||||
"msgs_streamed": 0,
|
||||
"events_streamed": null,
|
||||
"pty_bytes_total": 29189,
|
||||
"pty_data_callbacks": 23,
|
||||
"first_byte_ms": 126,
|
||||
"session_create_ms": 176,
|
||||
"stream_start_ms": null,
|
||||
"vmhwm_kb": 107336,
|
||||
"cg_peak": null,
|
||||
"drain_max_loop_lag_ms": 3,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"meta": {
|
||||
"cell": "startup",
|
||||
"ui": "opentui",
|
||||
"config": "otui-capped",
|
||||
"mode": "startup",
|
||||
"rep": 1,
|
||||
"run_id": "mq8k3b9i-cl30",
|
||||
"utc": "2026-06-10T21:03:47.143Z",
|
||||
"sha": "197d49948",
|
||||
"node": "/home/daimon/.local/share/fnm/node-versions/v26.3.0/installation/bin/node",
|
||||
"node_version": "v26.3.0",
|
||||
"pty": {
|
||||
"cols": 120,
|
||||
"rows": 40,
|
||||
"term": "xterm-256color"
|
||||
},
|
||||
"heap_mb": 8192,
|
||||
"memory_max": null,
|
||||
"container_cap": false,
|
||||
"container_memory": null,
|
||||
"opentui_cap": 3000,
|
||||
"fixture": {
|
||||
"path": "",
|
||||
"msgs": 0,
|
||||
"sha256": ""
|
||||
},
|
||||
"sample_every": 100,
|
||||
"mode_params": {},
|
||||
"ui_pid": 2373796,
|
||||
"gw_pid": 2373805,
|
||||
"cgroup": null,
|
||||
"load_avg_at_start": [
|
||||
0.28,
|
||||
0.51,
|
||||
0.46
|
||||
],
|
||||
"instrumented": false
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 25,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 0,
|
||||
"pty_writes": 0,
|
||||
"rss_kb": 53764,
|
||||
"pss_kb": 27358,
|
||||
"private_dirty_kb": 16248,
|
||||
"vmhwm_kb": 53916,
|
||||
"utime_ticks": 1,
|
||||
"stime_ticks": 1
|
||||
},
|
||||
{
|
||||
"kind": "periodic",
|
||||
"t_ms": 1036,
|
||||
"msgs": null,
|
||||
"events": null,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 108848,
|
||||
"pss_kb": 68551,
|
||||
"private_dirty_kb": 51992,
|
||||
"vmhwm_kb": 108848,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 3
|
||||
},
|
||||
{
|
||||
"kind": "final",
|
||||
"t_ms": 1142,
|
||||
"msgs": 0,
|
||||
"events": 0,
|
||||
"pty_bytes": 28911,
|
||||
"pty_writes": 14,
|
||||
"rss_kb": 108848,
|
||||
"pss_kb": 68551,
|
||||
"private_dirty_kb": 51992,
|
||||
"vmhwm_kb": 108848,
|
||||
"utime_ticks": 17,
|
||||
"stime_ticks": 3
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "session.create",
|
||||
"t_ms": 175
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "startup.catalog",
|
||||
"t_ms": 175
|
||||
},
|
||||
{
|
||||
"kind": "rpc",
|
||||
"method": "model.options",
|
||||
"t_ms": 175
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"result": "completed",
|
||||
"cap_hit": false,
|
||||
"cap_hit_basis": null,
|
||||
"at_messages": null,
|
||||
"exit": {
|
||||
"exitCode": 0,
|
||||
"signal": 0,
|
||||
"t": 1324
|
||||
},
|
||||
"stream_done": false,
|
||||
"msgs_streamed": 0,
|
||||
"events_streamed": null,
|
||||
"pty_bytes_total": 29189,
|
||||
"pty_data_callbacks": 18,
|
||||
"first_byte_ms": 128,
|
||||
"session_create_ms": 175,
|
||||
"stream_start_ms": null,
|
||||
"vmhwm_kb": 108848,
|
||||
"cg_peak": null,
|
||||
"drain_max_loop_lag_ms": 2,
|
||||
"drain_lag_violations": 0,
|
||||
"drain_ok": true,
|
||||
"digest": null
|
||||
}
|
||||
}
|
||||