Skip to content

feat: add Hermes gateway provider#2

Merged
eimexdev merged 3 commits into
mainfrom
feat/hermes-gateway-v1
Jul 24, 2026
Merged

feat: add Hermes gateway provider#2
eimexdev merged 3 commits into
mainfrom
feat/hermes-gateway-v1

Conversation

@eimexdev

@eimexdev eimexdev commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • add Hermes as a first-class T3 provider backed by an authenticated local gateway bridge
  • bundle a Hermes platform plugin for threads, streaming, interrupts, approvals, clarifications, slash commands, proactive/cron delivery, handoff threads, and inline images
  • add restart-safe callback delivery, write-ahead ingress idempotency, durable completion replay, and source-turn correlation for queued and concurrent work
  • keep the existing T3 UI largely unchanged, adding only provider settings, command discovery, model/provider visibility, and reusable inline image rendering

Architecture

T3 talks to an already-running Hermes gateway over an authenticated loopback HTTP ingress. Hermes remains the owner of agent execution, sessions, tools, commands, cron, memory, and restart behavior. Hermes callbacks enter the authenticated T3 server bridge and are projected into normal T3 threads and runtime events; no Hermes process or working directory is managed by T3.

Validation

  • Hermes plugin: 22 tests passed
  • server: 1,596 passed, 7 skipped
  • web: 1,446 passed
  • mobile: 529 passed
  • desktop: 366 passed
  • relay: 187 passed
  • package suites: contracts, shared, client runtime, Effect ACP/app server, SSH, and Tailscale passed
  • recursive typecheck passed
  • lint passed with existing warnings only
  • formatting check passed (excluding two pre-existing research Markdown formatting issues)
  • production build passed
  • independent Standards and V1 Spec adversarial reviews found no remaining actionable issues

Summary by CodeRabbit

  • New Features
    • Added Hermes as a supported provider, including server-side ingestion/projection, structured attachments, interactive prompts, message streaming, and image handling.
    • Added authenticated Hermes bridge integration (message/turn lifecycle, approvals/clarifications/confirmations, and thread creation) plus T3 Agent gateway plugin/contract support.
    • Introduced T3 Agent mode in the web UI (Hermes-only provider experience, agent-aware header/sidebar/composer).
    • Added provider-aware slash commands with Hermes precedence.
  • Bug Fixes
    • Assistant finalization now correctly replaces streamed previews (including intentionally empty final text) and projects completion attachments/final text reliably.
    • Image-only assistant messages no longer show an “empty response” label.
    • Freeform-only questions are preserved during user-input parsing.
  • Documentation
    • Added Hermes T3 Agent integration documentation and configuration guidance.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Assistant completion and Hermes bridge contracts

Layer / File(s) Summary
Bridge contracts and settings
packages/contracts/src/hermesBridge.ts, packages/contracts/src/settings.ts, packages/contracts/src/orchestration.ts
Adds Hermes wire schemas, provider settings, exports, and completion payload fields, with schema validation coverage.
Assistant completion projection
apps/server/src/orchestration/...
Carries authoritative final text and attachments through completion and supports replacing streamed text with empty final text.

Hermes server provider

Layer / File(s) Summary
Bridge transport and callback routing
apps/server/src/provider/hermes/*, apps/server/src/server.ts
Adds authenticated bridge requests, receiver registration, callback routing, HTTP handling, and orchestration dispatch.
Hermes session adapter
apps/server/src/provider/Layers/HermesAdapter.ts
Implements session lifecycle, streaming, turn correlation, interactive responses, image persistence, and callback projection.
Provider registration
apps/server/src/provider/Drivers/HermesDriver.ts, apps/server/src/provider/builtInDrivers.ts
Registers Hermes capabilities, snapshots, refresh behavior, and built-in provider wiring.

Hermes T3 Agent platform and web experience

Layer / File(s) Summary
Platform ingress and delivery
integrations/hermes/t3agent/*
Adds authenticated loopback ingress, durable idempotency, completion outbox delivery, interactive requests, images, handoffs, and plugin configuration.
T3 Agent product mode
apps/web/src/productMode.ts, apps/web/src/components/*, apps/web/src/routes/__root.tsx
Adds agent-mode detection and changes branding, provider selection, navigation, sidebar, settings, and chat controls.
Slash commands and attachments
apps/web/src/components/chat/*, apps/web/src/composer-logic.ts, apps/web/src/session-logic.ts
Adds provider command precedence, shared image rendering, image-only assistant handling, and freeform input preservation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant T3Agent
  participant HermesBridge
  participant HermesAdapter
  participant ServerOrchestration
  participant WebClient
  T3Agent->>HermesBridge: Submit message or interaction
  HermesBridge->>HermesAdapter: Deliver authenticated callback
  HermesAdapter->>ServerOrchestration: Publish runtime events
  ServerOrchestration->>WebClient: Stream text and attachments
  HermesAdapter->>HermesBridge: Send completion or interactive response
Loading

Suggested reviewers: juliusmarminge

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary, architecture, and validation, but it does not follow the required template sections and omits explicit UI Changes and Checklist entries. Rewrite the PR description using the repository template: What Changed, Why, UI Changes (or delete if not applicable), and the Checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding a Hermes gateway provider.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hermes-gateway-v1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 566b963924

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +396 to +400
ingressToken: TrimmedString.pipe(
Schema.withDecodingDefault(Effect.succeed("")),
Schema.annotateKey({
title: "Hermes ingress token",
description: "Secret T3 Agent uses when sending requests to the local Hermes plugin.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact Hermes bridge tokens before sending settings

When a user configures Hermes, this token (and the adjacent callback token) is stored in settings.providers.hermes or an instance config, but redactServerSettingsForClient only redacts providerInstances.environment before server.getConfig and settingsUpdated send the settings to the web client. Any paired/browser client can therefore read the bearer secrets and impersonate either side of the Hermes bridge; these password fields need the same redacted/placeholder treatment before leaving the server.

Useful? React with 👍 / 👎.

Comment on lines +1102 to +1104
message_id = _metadata_value(metadata, "messageId", "message_id") or _canonical_id(
"message", {"chatId": chat_id, "replyTo": reply_to, "content": content}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Namespace generated Hermes message IDs by thread

For Hermes sends that do not supply an explicit messageId, the fallback ID is derived only from chatId, replyTo, and content; in T3 all thread replies use the same chatId (t3agent), so two different threads receiving the same short response (for example Done) with the same replyTo state generate the same message id. The server projection upserts messages by global message_id, so the later delivery can overwrite/move the earlier thread's projected row; include the T3 thread id (or another per-thread namespace) in the canonical ID.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/src/session-logic.ts (1)

447-453: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not treat malformed non-empty options as freeform input.

With this guard removed, options: [{}] parses to options: [] and is accepted as a freeform question. Only an originally empty array should have that meaning; reject non-empty arrays whose entries all fail validation.

Proposed fix
       const options = question.options
         .map<UserInputQuestion["options"][number] | null>((option) => {
@@
         })
         .filter((option): option is UserInputQuestion["options"][number] => option !== null);
+      if (question.options.length > 0 && options.length === 0) {
+        return null;
+      }
       return {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/session-logic.ts` around lines 447 - 453, The question option
mapping must distinguish an originally empty options array from a non-empty
array whose entries are all invalid. Update the logic surrounding the returned
object and its options validation so malformed non-empty options are rejected
rather than normalized to freeform input, while preserving freeform behavior
only when the original options array is empty.
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts (1)

1612-1620: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

shouldSkipRedundantCompletion ignores attachments, can drop an attachments-only completion.

const shouldSkipRedundantCompletion =
  Option.isNone(activeAssistantMessageId) &&
  turnId !== undefined &&
  hasAssistantMessagesForTurn &&
  (assistantCompletion.fallbackText?.trim().length ?? 0) === 0;

This only checks fallbackText. If an item.completed for assistant_message arrives with only attachments (no detail/finalText) while the turn already has a prior assistant message and no active segment, this condition is true and finalizeAssistantMessage is skipped entirely — silently dropping the attachments. None of the three new tests in ProviderRuntimeIngestion.test.ts exercise this because they all omit turnId.

🐛 Proposed fix
         const shouldSkipRedundantCompletion =
           Option.isNone(activeAssistantMessageId) &&
           turnId !== undefined &&
           hasAssistantMessagesForTurn &&
-          (assistantCompletion.fallbackText?.trim().length ?? 0) === 0;
+          (assistantCompletion.fallbackText?.trim().length ?? 0) === 0 &&
+          (assistantCompletion.attachments?.length ?? 0) === 0;

Consider also adding a regression test with a turnId set, a prior assistant message for that turn, and a follow-up item.completed carrying only attachments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` around
lines 1612 - 1620, Update shouldSkipRedundantCompletion in
ProviderRuntimeIngestion so attachment-only assistant completions are not
skipped: require the completion to have neither non-empty fallbackText nor
attachments before treating it as redundant. Preserve the existing turn,
prior-message, and inactive-segment checks, and add a regression test covering a
set turnId, an existing assistant message, and a follow-up item.completed
containing only attachments.
🧹 Nitpick comments (4)
apps/server/src/provider/hermes/HermesBridgeRegistry.ts (1)

23-30: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Prefer crypto.timingSafeEqual over a hand-rolled constant-time compare.

Works correctly, but delegating to Node's audited timingSafeEqual (on same-length buffers, with a fast pre-check for length) avoids maintaining custom cryptographic-adjacent code.

🔒 Proposed refactor
+import { timingSafeEqual } from "node:crypto";
+
-function constantTimeEqual(left: string, right: string): boolean {
-  const maximumLength = Math.max(left.length, right.length);
-  let difference = left.length ^ right.length;
-  for (let index = 0; index < maximumLength; index += 1) {
-    difference |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
-  }
-  return difference === 0;
-}
+function constantTimeEqual(left: string, right: string): boolean {
+  const leftBuffer = Buffer.from(left);
+  const rightBuffer = Buffer.from(right);
+  return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/hermes/HermesBridgeRegistry.ts` around lines 23 -
30, Replace the hand-rolled constantTimeEqual implementation with Node’s
crypto.timingSafeEqual, returning false immediately when the input lengths
differ and comparing same-length Buffer representations otherwise. Preserve the
function’s boolean equality contract and update imports as needed.
apps/server/src/provider/hermes/http.ts (1)

86-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sentinel-property error signaling is fragile.

Bridge errors are converted to { _bridgeError: true, status } success values and then detected via property-sniffing on an otherwise-unknown result. Consider an explicit Effect.result/tagged-union return from HermesBridgeRegistry.receive (or keeping the error in the E channel and handling it with Effect.catchTag/pattern matching) instead of mixing a synthetic sentinel into the same channel as real receiver payloads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/hermes/http.ts` around lines 86 - 102, The receive
handling around HermesBridgeRegistry.receive should stop converting bridge
failures into synthetic {_bridgeError, status} success objects and detecting
them by property sniffing. Preserve bridge errors as typed Effect failures and
handle the relevant error variants with Effect.catchTag or explicit pattern
matching, while returning normal receiver payloads unchanged.
apps/server/src/provider/Layers/HermesAdapter.ts (1)

412-454: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Persist images before emitting content.delta to keep redelivery idempotent.

For final messages, content.delta is published before persistImages; if persistence fails, the delivery ID is not committed, so Hermes can redeliver and re-emit the same hermes:${deliveryId}:content event. A failed persistence should emit nothing so a retry starts clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/HermesAdapter.ts` around lines 412 - 454,
Update handleMessageCallback so final-message image persistence via
persistImages completes before publishing any content.delta event. If
persistence fails, propagate the failure before emitting or updating message
state, allowing Hermes redelivery to retry without duplicate events; preserve
the existing behavior for non-final messages.
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts (1)

47-59: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Malformed attachments are dropped with no diagnostic trail.

attachmentsFromItemData filters out any array entries that fail isChatAttachment and only returns undefined when nothing survives — a partially-malformed payload from Hermes (e.g., a bad mimeType) silently loses just that attachment with no log line to help diagnose it in production.

♻️ Suggested addition
   const parsed = attachments.filter(isChatAttachment);
+  if (parsed.length !== attachments.length) {
+    // eslint-disable-next-line no-console -- or use the project's structured logger
+    console.warn("dropped invalid Hermes assistant attachment(s)", {
+      total: attachments.length,
+      valid: parsed.length,
+    });
+  }
   return parsed.length > 0 ? parsed : undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` around
lines 47 - 59, Update attachmentsFromItemData to detect and log malformed
entries when filtering attachments with isChatAttachment, while preserving valid
attachments and the existing undefined behavior when none survive. Use the
established logging mechanism available in the surrounding ingestion flow and
include enough context to diagnose the rejected attachment data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/provider/hermes/HermesBridgeClient.ts`:
- Line 49: Add a bounded Effect.timeout to both Hermes bridge request paths,
getCapabilities and send, around the execute flow created from
HttpClient.filterStatusOk. Convert timeout failures into
ProviderAdapterRequestError and preserve existing error handling for other
failures; define an explicit retry policy only if these requests are intended to
retry transient errors.

In `@apps/server/src/provider/hermes/http.ts`:
- Around line 105-108: Scope Hermes thread and thread.create command identifiers
by both instance and delivery. Update the threadId construction near
stableIdSegment and the corresponding commandId at the thread.create call to
reuse a deterministic instance-scoped identifier, while preserving projectId’s
existing instance scoping and delivery-based uniqueness.
- Around line 24-38: The isThreadCreateCallback guard in the receiver handling
path validates only the type discriminant and permits malformed data to reach
stableIdSegment and HTTP response processing. Replace this predicate-based
narrowing with decoding against the existing
HermesBridgeThreadCreateRequest/response schema, validating protocolVersion,
requestId, deliveryId, parentChatId, name, and occurrenceId, and construct
branded IDs through their factories rather than casts; ensure decode defects are
handled through the Effect failure path, adding catchDefect handling if
unchecked operations remain.

In `@apps/web/src/composer-logic.ts`:
- Around line 281-289: Update providerAdvertisesSlashCommand so each provider
command name is normalized with the same leading-slash removal, trimming, and
lowercasing as normalizedCommandName before comparison. Preserve the existing
some-based matching behavior.

In `@integrations/hermes/t3agent/adapter.py`:
- Around line 545-547: Both _persist_ingress_ledger
(integrations/hermes/t3agent/adapter.py#L545-L547) and
_persist_completion_outbox (integrations/hermes/t3agent/adapter.py#L555-L560)
must create temporary state files with owner-only permissions from the outset.
Replace the current write_text-then-chmod flow at both sites with an
os.open-based write using O_WRONLY|O_CREAT|O_TRUNC and mode 0o600 before
replace, preserving the existing atomic replacement behavior.
- Around line 1128-1132: Bound _message_destinations using the same capped
OrderedDict strategy as _completed_requests: update its declaration to the
appropriate ordered, bounded mapping and evict the oldest entry immediately
after inserting a new destination in the send/edit flow. Preserve the existing
destination data and eviction limit used by _completed_requests.
- Around line 749-809: Update _run_once so self._idempotency_lock is held only
while checking cached requests and durably recording the pending claim; release
it before awaiting operation(). After operation() completes or fails, re-acquire
the lock to record and persist the terminal completed/failed state, preserving
duplicate handling and cache updates without blocking urgent ingress work during
_message_handler or cancel_session_processing.

---

Outside diff comments:
In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`:
- Around line 1612-1620: Update shouldSkipRedundantCompletion in
ProviderRuntimeIngestion so attachment-only assistant completions are not
skipped: require the completion to have neither non-empty fallbackText nor
attachments before treating it as redundant. Preserve the existing turn,
prior-message, and inactive-segment checks, and add a regression test covering a
set turnId, an existing assistant message, and a follow-up item.completed
containing only attachments.

In `@apps/web/src/session-logic.ts`:
- Around line 447-453: The question option mapping must distinguish an
originally empty options array from a non-empty array whose entries are all
invalid. Update the logic surrounding the returned object and its options
validation so malformed non-empty options are rejected rather than normalized to
freeform input, while preserving freeform behavior only when the original
options array is empty.

---

Nitpick comments:
In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts`:
- Around line 47-59: Update attachmentsFromItemData to detect and log malformed
entries when filtering attachments with isChatAttachment, while preserving valid
attachments and the existing undefined behavior when none survive. Use the
established logging mechanism available in the surrounding ingestion flow and
include enough context to diagnose the rejected attachment data.

In `@apps/server/src/provider/hermes/HermesBridgeRegistry.ts`:
- Around line 23-30: Replace the hand-rolled constantTimeEqual implementation
with Node’s crypto.timingSafeEqual, returning false immediately when the input
lengths differ and comparing same-length Buffer representations otherwise.
Preserve the function’s boolean equality contract and update imports as needed.

In `@apps/server/src/provider/hermes/http.ts`:
- Around line 86-102: The receive handling around HermesBridgeRegistry.receive
should stop converting bridge failures into synthetic {_bridgeError, status}
success objects and detecting them by property sniffing. Preserve bridge errors
as typed Effect failures and handle the relevant error variants with
Effect.catchTag or explicit pattern matching, while returning normal receiver
payloads unchanged.

In `@apps/server/src/provider/Layers/HermesAdapter.ts`:
- Around line 412-454: Update handleMessageCallback so final-message image
persistence via persistImages completes before publishing any content.delta
event. If persistence fails, propagate the failure before emitting or updating
message state, allowing Hermes redelivery to retry without duplicate events;
preserve the existing behavior for non-final messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d4647830-a177-460e-a57f-0d4de39a89b7

📥 Commits

Reviewing files that changed from the base of the PR and between 94bf633 and 566b963.

📒 Files selected for processing (43)
  • apps/server/src/orchestration/Layers/ProjectionPipeline.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/orchestration/projector.ts
  • apps/server/src/provider/Drivers/HermesDriver.test.ts
  • apps/server/src/provider/Drivers/HermesDriver.ts
  • apps/server/src/provider/Layers/HermesAdapter.test.ts
  • apps/server/src/provider/Layers/HermesAdapter.ts
  • apps/server/src/provider/Layers/ProviderRegistry.test.ts
  • apps/server/src/provider/builtInDrivers.ts
  • apps/server/src/provider/hermes/HermesBridgeClient.test.ts
  • apps/server/src/provider/hermes/HermesBridgeClient.ts
  • apps/server/src/provider/hermes/HermesBridgeRegistry.test.ts
  • apps/server/src/provider/hermes/HermesBridgeRegistry.ts
  • apps/server/src/provider/hermes/http.ts
  • apps/server/src/server.test.ts
  • apps/server/src/server.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/chat/ChatComposer.tsx
  • apps/web/src/components/chat/ExpandedImagePreview.test.ts
  • apps/web/src/components/chat/MessagesTimeline.test.tsx
  • apps/web/src/components/chat/MessagesTimeline.tsx
  • apps/web/src/components/chat/composerSlashCommandSearch.test.ts
  • apps/web/src/components/chat/composerSlashCommandSearch.ts
  • apps/web/src/components/chat/providerIconUtils.ts
  • apps/web/src/components/settings/ProviderSettingsForm.test.ts
  • apps/web/src/components/settings/providerDriverMeta.ts
  • apps/web/src/composer-logic.test.ts
  • apps/web/src/composer-logic.ts
  • apps/web/src/session-logic.test.ts
  • apps/web/src/session-logic.ts
  • integrations/hermes/t3agent/README.md
  • integrations/hermes/t3agent/adapter.py
  • integrations/hermes/t3agent/plugin.yaml
  • integrations/hermes/t3agent/tests/test_adapter.py
  • packages/contracts/package.json
  • packages/contracts/src/hermesBridge.test.ts
  • packages/contracts/src/hermesBridge.ts
  • packages/contracts/src/index.ts
  • packages/contracts/src/model.ts
  • packages/contracts/src/orchestration.ts
  • packages/contracts/src/settings.ts

readonly httpClient: HttpClient.HttpClient;
}): HermesBridgeClient {
const baseUrl = normalizeBaseUrl(input.baseUrl);
const execute = input.httpClient.pipe(HttpClient.filterStatusOk);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching HermesBridgeClient.ts:"
fd -a 'HermesBridgeClient\.ts$' . || true

file="$(fd 'HermesBridgeClient\.ts$' . | head -n 1 || true)"
if [ -n "${file:-}" ]; then
  echo
  echo "File: $file"
  wc -l "$file"
  echo
  sed -n '1,130p' "$file" | cat -n
fi

echo
echo "Search for HermesBridgeClient and timeout usage:"
rg -n "HermesBridgeClient|makeHermesBridgeClient|Effect\.timeout|HttpClient\.retryTransient|retryTransient|timeout" apps/server/src/provider/hermes apps/server/src -S || true

echo
echo "Package references to effect/http:"
rg -n '"effect"|unstable/http|`@effect/http`' package.json apps -g 'package.json' -S || true

Repository: eimexdev/t3agent

Length of output: 27504


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Lockfile effect entries:"
if [ -f pnpm-lock.yaml ]; then
  rg -n 'effect(@|:)?|id: .*effect' pnpm-lock.yaml -A2 -B2 | head -n 80 || true
fi
if [ -f package-lock.json ]; then
  node - <<'JS'
const fs=require('fs');
const lock=JSON.parse(fs.readFileSync('package-lock.json','utf8'));
const pkg=lock.packages?.['node_modules/effect'];
console.log(pkg?.version);
const serversPkg=lock.packages?.['apps/server/node_modules/effect'];
console.log('apps/server version:', serversPkg?.version);
JS
fi

echo
echo "Hermes adapter/client usages:"
sed -n '120,170p' apps/server/src/provider/Drivers/HermesDriver.ts | cat -n
sed -n '245,305p' apps/server/src/provider/Layers/HermesAdapter.ts | cat -n

echo
echo "HermsBridgeClient tests around timeout/mock behavior:"
sed -n '1,120p' apps/server/src/provider/hermes/HermesBridgeClient.test.ts | cat -n

Repository: eimexdev/t3agent

Length of output: 13848


🌐 Web query:

effect unstable http HttpClient documentation timeout retryTransient Effect.timeout

💡 Result:

In Effect-TS, HttpClient.retryTransient is a dedicated combinator for retrying requests that fail due to temporary, transient issues [1]. Its relationship with timeouts and error handling is as follows: 1. Handling of TimeoutException: HttpClient.retryTransient is designed to automatically retry failures caused by a TimeoutException (which is what Effect.timeout throws when a limit is exceeded) [1][2]. This allows for the automated handling of common network blips, server hiccups, and timed-out requests without needing to manually define retry logic for every transient failure mode [1]. 2. Transient Error Classification: Beyond TimeoutException, HttpClient.retryTransient retries errors based on specific criteria [1]: - TransportError: Network-level failures such as DNS resolution errors or connection refusals [1]. - StatusCodeError: HTTP responses with specific status codes deemed transient: 408 (Request Timeout), 429 (Too Many Requests), 500 (Internal Server Error), 502 (Bad Gateway), 503 (Service Unavailable), and 504 (Gateway Timeout) [1][3][4]. 3. Implementation Details: The "unstable" HttpClient module (often found under @effect/platform or similar paths in the Effect repository) provides this functionality [5][6]. Recent updates have refined the response-level retry logic to use an explicit allowlist of these transient status codes, ensuring that permanent errors (like 501 Not Implemented) are not retried [3][4][7]. If you are using Effect.timeout in conjunction with a custom HttpClient, ensure that your timeout handling strategy aligns with how these exceptions are propagated. While Effect.timeout can be used to wrap requests, using HttpClient.retryTransient ensures that those specific TimeoutExceptions are correctly recognized as transient and subsequently retried according to your configured schedule [1][8].

Citations:


🌐 Web query:

effect 4.0 HttpClient source no timeout retryTransient TimeoutException

💡 Result:

In Effect 4.0, the HttpClient.retryTransient() combinator is specifically designed to automatically retry requests that fail due to transient issues [1][2]. It handles both network-level errors and specific HTTP status codes that are considered temporary [1][3]. Transient Errors Covered: The built-in retry logic includes: 1. TimeoutException: Errors originating from Effect.timeout [1]. 2. Transport Errors: Network failures (e.g., DNS issues, connection refused) classified as TransportError [1]. 3. Transient HTTP Status Codes: Recent updates to the retryTransient implementation use an explicit allowlist for status codes deemed retryable [4][3]: - 408 (Request Timeout) - 429 (Too Many Requests) - 500 (Internal Server Error) - 502 (Bad Gateway) - 503 (Service Unavailable) - 504 (Gateway Timeout) Configuration and Customization: - While Predicate: You can extend or refine which errors are considered transient by providing a while predicate in the options [1]. This is checked in addition to the default transient criteria [1]. Note that in v4, the while predicate applies specifically to error retries [4]. - Mode: You can control whether the schedule considers both errors and responses, or just one, using the mode option (e.g., "errors-only" or "response-only") [1]. If you find that specific transient errors are not being retried as expected, verify that they are included in the above list or use the while predicate to explicitly include them [1][5]. Previous versions had issues where codes like 408 were missing or 501 were incorrectly included; these have been addressed in v4 [4][3][5].

Citations:


Add a per-request timeout to Hermes bridge calls.

getCapabilities and send call out to the Hermes bridge without a bounded timeout, so a hung/local gateway can block the provider operation indefinitely. Add Effect.timeout(...) around both execution paths and map timeout errors to ProviderAdapterRequestError; include an explicit retry policy if you intend to retry transient failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/hermes/HermesBridgeClient.ts` at line 49, Add a
bounded Effect.timeout to both Hermes bridge request paths, getCapabilities and
send, around the execute flow created from HttpClient.filterStatusOk. Convert
timeout failures into ProviderAdapterRequestError and preserve existing error
handling for other failures; define an explicit retry policy only if these
requests are intended to retry transient errors.

Comment on lines +24 to +38
const isThreadCreateCallback = (
value: unknown,
): value is {
readonly type: "thread.create";
readonly protocolVersion: 1;
readonly requestId: string;
readonly deliveryId: string;
readonly parentChatId: string;
readonly name: string;
} =>
typeof value === "object" &&
value !== null &&
"type" in value &&
(value as { readonly type?: unknown }).type === "thread.create";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)http\.ts$|server\.test\.ts$|package\.json$|pnpm-lock\.yaml$|yarn\.lock$|package-lock\.json$' | sed -n '1,120p'

echo "== http.ts outline =="
ast-grep outline apps/server/src/provider/hermes/http.ts --view expanded || true

echo "== http.ts relevant lines =="
cat -n apps/server/src/provider/hermes/http.ts | sed -n '1,190p'

echo "== tests around Hermes receivers =="
rg -n "hermes|thread\.create|cron-delivery|register|receive" apps/server/src provider apps -g 'server.test.ts' -g '*server*' -g '*test*' | sed -n '1,220p'

Repository: eimexdev/t3agent

Length of output: 39631


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== HermeBridgeRegistry files =="
fd -a 'HermesBridgeRegistry|HermesBridgeClient|HermesDriver' apps/server/src/provider/hermes apps/server/src/provider -e ts -e test.ts | sed 's#^\./##'

echo "== HermesAdapter relevant lines =="
cat -n apps/server/src/provider/Layers/HermesAdapter.test.ts | sed -n '1,290p'

echo "== Contracts HermesBridge relevant files =="
fd -a 'hermesBridge|HermesBridge' packages/contracts packages 2>/dev/null | sed -n '1,120p'

echo "== package versions effect `@t3tools` contracts =="
cat package.json apps/server/package.json packages/contracts/package.json | sed -n '1,220p'

Repository: eimexdev/t3agent

Length of output: 18704


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/contracts/hermesBridge.ts =="
cat -n packages/contracts/src/hermesBridge.ts | sed -n '1,360p'

echo "== packages/contracts/hermesBridge.test.ts relevant lines =="
cat -n packages/contracts/src/hermesBridge.test.ts | sed -n '1,240p'

echo "== HermesBridgeRegistry.ts =="
cat -n apps/server/src/provider/hermes/HermesBridgeRegistry.ts | sed -n '1,240p'

echo "== HermesBridgeRegistry.test.ts lines =="
cat -n apps/server/src/provider/hermes/HermesBridgeRegistry.test.ts | sed -n '1,220p'

echo "== effect version catalog/lock entries =="
rg -n '"effect"|"`@effect/`' pnpm-lock.yaml package.json | sed -n '1,220p'

Repository: eimexdev/t3agent

Length of output: 34096


🌐 Web query:

Effect version 3.14 catch catchAll defects Effect.catch documentation

💡 Result:

In Effect version 4.0 and later, Effect.catchAll has been renamed to Effect.catch [1][2]. If you are working with an earlier version (e.g., v3.x), you should continue using Effect.catchAll [1][2]. Regarding the handling of defects: 1. Relationship between catch and catchAll: They are effectively the same method; catch is the updated name for catchAll to simplify the API [2]. Neither is designed to catch "defects" (which are typically fatal errors or unrecoverable issues like unexpected exceptions) by default [3][1]. 2. Handling Defects: Defects are generally managed using specific "defect-aware" combinators [4][2]. To catch or handle defects specifically, you should use: - Effect.catchAllDefect (in versions prior to v4.0) [2] - Effect.catchDefect (in v4.0 and later) [2] If an error is in the "error channel" (an expected failure), Effect.catch is the correct tool [5][2]. If you are encountering unexpected runtime exceptions or defects, you must use catchDefect (or its equivalent in your version) to intercept them [2]. Note that standard catch logic only operates on the typed error channel, not on defects [1]. For migration purposes, the general pattern in the Effect library is that catchAll* methods have been renamed to catch* [1][2]. If you are upgrading from v3 to v4, update your code as follows: - Effect.catchAllEffect.catch [2] - Effect.catchAllDefectEffect.catchDefect [2]

Citations:


Harden thread.create callback handling against malformed receiver output.

isThreadCreateCallback only checks the type discriminant, but result is still unknown from receive() and stableIdSegment() assumes the field exists and is a string. A malformed thread create result can throw before HTTP response handling, bypassing Effect.catch’s typed failure channel; add catchDefect/defect handling too if keeping this unchecked path.

The existing contracts use a real HermesBridgeThreadCreateRequest/response schema including protocolVersion, requestId, deliveryId, parentChatId, name, and occurrenceId; decode the receiver result instead of relying on this predicate so requestId/deliveryId can be constructed with the branded ID factories instead of as never.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/hermes/http.ts` around lines 24 - 38, The
isThreadCreateCallback guard in the receiver handling path validates only the
type discriminant and permits malformed data to reach stableIdSegment and HTTP
response processing. Replace this predicate-based narrowing with decoding
against the existing HermesBridgeThreadCreateRequest/response schema, validating
protocolVersion, requestId, deliveryId, parentChatId, name, and occurrenceId,
and construct branded IDs through their factories rather than casts; ensure
decode defects are handled through the Effect failure path, adding catchDefect
handling if unchecked operations remain.

Comment on lines +105 to +108
const instanceSegment = stableIdSegment(instanceId);
const deliverySegment = stableIdSegment(result.deliveryId);
const projectId = ProjectId.make(`t3-agent-${instanceSegment}`);
const threadId = ThreadId.make(`hermes-${deliverySegment}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

threadId/commandId collide across different Hermes instances.

projectId is scoped by instanceSegment (t3-agent-${instanceSegment}), but threadId (line 108) and the thread.create commandId (line 120) are derived only from deliveryId, with no instance scoping. Two different registered Hermes instances sending the same deliveryId (plausible given test payloads use plain labels like "cron-delivery-1", not UUIDs) would collide on the same threadId/commandId, causing the orchestration engine to either silently dedupe a genuinely new thread from the second instance, or conflate two unrelated threads.

🔧 Proposed fix: scope thread/command identifiers by instance too
-          const threadId = ThreadId.make(`hermes-${deliverySegment}`);
+          const threadId = ThreadId.make(`hermes-${instanceSegment}-${deliverySegment}`);
...
-            commandId: CommandId.make(`hermes:${result.deliveryId}:thread-create`),
+            commandId: CommandId.make(`hermes:${instanceId}:${result.deliveryId}:thread-create`),

Also applies to: 111-111, 120-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/hermes/http.ts` around lines 105 - 108, Scope Hermes
thread and thread.create command identifiers by both instance and delivery.
Update the threadId construction near stableIdSegment and the corresponding
commandId at the thread.create call to reuse a deterministic instance-scoped
identifier, while preserving projectId’s existing instance scoping and
delivery-based uniqueness.

Comment on lines +281 to +289
export function providerAdvertisesSlashCommand(
providerSlashCommands: ReadonlyArray<{ readonly name: string }>,
commandName: string,
): boolean {
const normalizedCommandName = commandName.replace(/^\/+/, "").trim().toLowerCase();
return providerSlashCommands.some(
(command) => command.name.trim().toLowerCase() === normalizedCommandName,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize leading slashes on provider command names too.

Line 287 leaves a provider-advertised "/plan" unchanged, so it fails to suppress T3’s built-in /plan handling and is never sent to Hermes.

Proposed fix
-    (command) => command.name.trim().toLowerCase() === normalizedCommandName,
+    (command) =>
+      command.name.replace(/^\/+/, "").trim().toLowerCase() === normalizedCommandName,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function providerAdvertisesSlashCommand(
providerSlashCommands: ReadonlyArray<{ readonly name: string }>,
commandName: string,
): boolean {
const normalizedCommandName = commandName.replace(/^\/+/, "").trim().toLowerCase();
return providerSlashCommands.some(
(command) => command.name.trim().toLowerCase() === normalizedCommandName,
);
}
export function providerAdvertisesSlashCommand(
providerSlashCommands: ReadonlyArray<{ readonly name: string }>,
commandName: string,
): boolean {
const normalizedCommandName = commandName.replace(/^\/+/, "").trim().toLowerCase();
return providerSlashCommands.some(
(command) =>
command.name.replace(/^\/+/, "").trim().toLowerCase() === normalizedCommandName,
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/composer-logic.ts` around lines 281 - 289, Update
providerAdvertisesSlashCommand so each provider command name is normalized with
the same leading-slash removal, trimming, and lowercasing as
normalizedCommandName before comparison. Preserve the existing some-based
matching behavior.

Comment on lines +545 to +547
temporary.write_text(json.dumps(records, separators=(",", ":")), encoding="utf-8")
temporary.chmod(0o600)
temporary.replace(path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Create durable state files owner-only from the start. Both persist helpers write_text the temp file (inheriting the process umask, typically world/group-readable) and only chmod(0o600) afterward, so there is a brief window where the ledger/outbox contents (thread/message data the README asks to keep owner-only) are readable by other local users. Open with 0o600 up front instead.

  • integrations/hermes/t3agent/adapter.py#L545-L547: in _persist_ingress_ledger, write via os.open(..., O_WRONLY|O_CREAT|O_TRUNC, 0o600) (or os.umask-guarded open) before replace, so the temp file is never created with broader permissions.
  • integrations/hermes/t3agent/adapter.py#L555-L560: apply the same owner-only creation to the completion-outbox temp file in _persist_completion_outbox.
📍 Affects 1 file
  • integrations/hermes/t3agent/adapter.py#L545-L547 (this comment)
  • integrations/hermes/t3agent/adapter.py#L555-L560
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/hermes/t3agent/adapter.py` around lines 545 - 547, Both
_persist_ingress_ledger (integrations/hermes/t3agent/adapter.py#L545-L547) and
_persist_completion_outbox (integrations/hermes/t3agent/adapter.py#L555-L560)
must create temporary state files with owner-only permissions from the outset.
Replace the current write_text-then-chmod flow at both sites with an
os.open-based write using O_WRONLY|O_CREAT|O_TRUNC and mode 0o600 before
replace, preserving the existing atomic replacement behavior.

Comment on lines +749 to +809
# Holding this lock across the short dispatch/resolver call also closes
# the race where two identical requests arrive before either is cached.
async with self._idempotency_lock:
cached = self._completed_requests.get(request_id)
if cached is not None:
self._completed_requests.move_to_end(request_id)
# A prior write may have failed while the process remained
# alive. Never acknowledge a duplicate until the ledger is
# durably synchronized.
self._persist_ingress_ledger()
if cached[2] == "pending":
failure = {
"protocolVersion": PROTOCOL_VERSION,
"requestId": request_id,
"error": (
"Hermes restarted or failed after durably claiming this request; "
"execution cannot be replayed safely."
),
}
self._completed_requests[request_id] = (409, failure, "failed")
self._persist_ingress_ledger()
return web.json_response(failure, status=409)
if cached[2] == "failed":
return web.json_response(cached[1], status=cached[0])
duplicate = dict(cached[1])
duplicate["status"] = "duplicate"
return web.json_response(duplicate, status=cached[0])
# Write the idempotency claim before dispatch. If Hermes exits at
# any later point, a replay is suppressed rather than risking the
# same agent/tool side effects twice. Validation has already run,
# so this is the narrow durable inbox boundary.
pending_body = _ack(request_id, "accepted")
self._completed_requests[request_id] = (
accepted_status,
pending_body,
"pending",
)
try:
self._persist_ingress_ledger()
except OSError:
self._completed_requests.pop(request_id, None)
raise
try:
status, body = await operation()
except Exception:
failure = {
"protocolVersion": PROTOCOL_VERSION,
"requestId": request_id,
"error": "Hermes failed while executing the durably claimed request.",
}
self._completed_requests[request_id] = (500, failure, "failed")
self._persist_ingress_ledger()
raise
self._completed_requests[request_id] = (status, body, "completed")
self._completed_requests.move_to_end(request_id)
while len(self._completed_requests) > DEFAULT_IDEMPOTENCY_CACHE_SIZE:
self._completed_requests.popitem(last=False)
# Persist before returning the acknowledgement so a T3 retry after
# a Hermes restart cannot execute an already-completed operation.
self._persist_ingress_ledger()
return web.json_response(body, status=status)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect base handle_message + bypass semantics to gauge how long operation() runs under the lock.
fd -t f 'base.py' integrations/hermes -x ast-grep outline {} --items all
rg -nP -C4 'def handle_message|def should_bypass_active_session|def cancel_session_processing' -g '!**/t3agent/**'

Repository: eimexdev/t3agent

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files matching adapter.py/base.py in integrations/hermes =="
fd -t f 'adapter.py|base.py' integrations/hermes || true

echo "== t3agent adapter outline =="
for f in $(fd -t f 'adapter.py|base.py' integrations/hermes); do
  echo "--- $f"
  wc -l "$f"
  ast-grep outline "$f" --items all || true
done

echo "== relevant lock/idempotency definitions =="
rg -n -C 5 'self\._idempotency_lock|_run_once|DEFAULT_IDEMPOTENCY_CACHE_SIZE|_completed_requests|handle_message|should_bypass_active_session|cancel_session_processing|messages=interrupt|slash|clarifications|approvals|confirmations' integrations/hermes || true

Repository: eimexdev/t3agent

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="integrations/hermes/t3agent/adapter.py"

echo "== handle_message implementation =="
sed -n '430,510p' "$file" | cat -n

echo "== _message_handler and bypass definitions =="
rg -n -C 6 '_message_handler|should_bypass_active_session|CommandProcessingOutcome|active_sessions|cancel_session_processing' platform gateway tools integrations -g '!**/t3agent/**' -g '!**/venv/**' -g '!**/.venv/**' || true

echo "== direct references to BasePlatformAdapter implementation candidates =="
rg -n -C 3 'class BasePlatformAdapter|def handle_message .*BasePlatformAdapter|def cancel_session_processing|should_bypass_active_session|CommandProcessingOutcome|ActiveSession' gateway platform tools integrations -g '!**/t3agent/**' -g '!**/venv/**' -g '!**/.venv/**' || true

Repository: eimexdev/t3agent

Length of output: 4772


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package references to base adapter =="
rg -n 'BasePlatformAdapter|class .*Adapter|from .*gateway|import .*gateway|gateway\.session|GatewayStreamConsumer' -g '!**/venv/**' -g '!**/.venv/**' .

echo "== imports in adapter.py =="
sed -n '1,55p' integrations/hermes/t3agent/adapter.py | cat -n

echo "== inspect command-related files mentioned in handle_message =="
python3 - <<'PY'
from pathlib import Path
roots = [Path.cwd()]
for root in roots:
    for p in root.rglob('*'):
        if p.is_file() and p.name in {'__init__.py','commands.py','slash_confirm.py','platform_adapter.py'}:
            if 'venv' in p.parts or '.venv' in p.parts:
                continue
            txt = p.read_text(errors='ignore')
            if any(s in txt for s in ['should_bypass_active_session','handle_message','_message_handler','BasePlatformAdapter','slash_confirm']):
                print(p)
PY

Repository: eimexdev/t3agent

Length of output: 9842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate gateway base platform =="
fd -t f 'base.py' . | grep 'gateway/platforms'

echo "== BasePlatformAdapter.handle_message =="
gateway_base=$(fd -t f 'base.py' . | grep 'gateway/platforms' | head -n1)
if [ -z "${gateway_base:-}" ]; then
  echo "gateway base not found"
  exit 0
fi
echo "BASE=$gateway_base"
rg -n -C 8 'class BasePlatformAdapter|Async for .*handle_message|def handle_message' "$gateway_base" || true

echo "== command bypass and handle references =="
for p in $(fd -t f 'commands.py|slash_confirm.py|approval.py|clarify_gateway.py' . | grep -v 'venv' | grep -v '.venv'); do
  echo "--- $p"
  rg -n -C 6 'should_bypass_active_session|def resolve_gateway_approval|def resolve_gateway_clarify|slash_confirm|ActiveSession|CommandProcessingOutcome|should_bypass.*session|busy' "$p" || true
done

echo "== T3AgentAdapter relevant async helpers =="
sed -n '390,430p' integrations/hermes/t3agent/adapter.py | cat -n
sed -n '550,630p' integrations/hermes/t3agent/adapter.py | cat -n

Repository: eimexdev/t3agent

Length of output: 189


🌐 Web query:

Hermes Agent gateway.platforms.base BasePlatformAdapter handle_message

💡 Result:

In the Hermes Agent architecture, the BasePlatformAdapter class is located in gateway/platforms/base.py and serves as the fundamental interface for all messaging platform integrations (e.g., Telegram, Discord, Slack, WhatsApp) [1][2][3]. The handle_message method is a core function of the BasePlatformAdapter that facilitates the dispatching of inbound messages [4][3]. When an adapter receives a message from its respective platform, it is responsible for invoking this method, which forwards the message event to the central gateway runner for processing by the AIAgent [4][5][3]. Key responsibilities and functions of BasePlatformAdapter and its handle_message flow include: - Message Routing: handle_message dispatches incoming messages to internal background tasks, manages active session tracking, and handles interrupt signaling [3]. - Standardized Interface: All custom platform adapters must inherit from BasePlatformAdapter and implement abstract methods such as connect, disconnect, and send [2][4][3]. - Integration Pattern: When developing a new adapter, developers are instructed to use self.handle_message(event) to push platform-specific events into the Hermes gateway pipeline [6]. - Async Handling: Because handle_message operates on the asyncio event loop, adapters must ensure that asynchronous platform callbacks (e.g., from WebSockets or Pub/Sub streams) safely schedule the handle_message call using patterns like asyncio.run_coroutine_threadsafe to avoid blocking the main event loop [7]. For developers, this structure ensures that platform-specific transport logic is decoupled from the agent's core conversational logic, allowing for extensibility without modifying the core Hermes codebase [2][6].

Citations:


🌐 Web query:

github.com/nousresearch/hermes-agent gateway/platforms/base.py "async def handle_message"

💡 Result:

In the NousResearch/hermes-agent repository, gateway/platforms/base.py contains the platform adapter logic, including the async def handle_message method [1]. This method serves as the core message handler for the platform gateway [1]. Specifically, it implements an active-session guard to manage agent processing states [1]. When an agent is actively processing a task, incoming messages (other than certain system commands) are typically queued to prevent conflicts [1][2]. Recent updates have introduced bypass logic within handle_message to allow specific system commands—such as /approve, /deny [2], and /status [1]—to be dispatched immediately even when the agent is busy. This prevents deadlocks (e.g., when the agent waits for approval) and ensures that critical status updates remain responsive [1][2]. When a command bypasses the guard, it is processed without altering the session lifecycle or interrupting the existing task [1][2].

Citations:


🌐 Web query:

github.com/nousresearch/hermes-agent "should_bypass_active_session" "BasePlatformAdapter"

💡 Result:

The identifier should_bypass_active_session does not appear in the public codebase of the NousResearch/hermes-agent repository [1][2][3][4]. The BasePlatformAdapter in the Hermes Agent project is responsible for managing platform-specific logic, including tracking active message sessions via an _active_sessions dictionary to support interrupt commands like /stop, /new, or /reset [4]. While the adapter handles session states and task cancellation to ensure deterministic management of "busy" states [4], there is no method or flag named should_bypass_active_session within the BasePlatformAdapter class or the broader Hermes Agent repository [3][4]. If you encountered this term, it may be: 1. A custom addition to a private fork of the repository. 2. A misremembered or hallucinated method name. 3. Code from an unrelated project, as identifiers like "bypass" and "session" are common in session store and authorization middleware patterns (e.g., in Rails or other framework components) [5].

Citations:


🌐 Web query:

file:gateway/platforms/base.py BasePlatformAdapter handle_message await self.hermes.run

💡 Result:

In the Hermes Agent framework, the BasePlatformAdapter class located in gateway/platforms/base.py serves as the foundational interface for all platform adapters [1][2]. While the specific implementation of message handling involves the adapter receiving data and subsequently forwarding it to the gateway, the pattern described—specifically invoking self.handle_message(event)—is the standard mechanism for platform adapters to pass inbound messages to the system's message processing pipeline [3][2]. Based on the architecture of Hermes Agent: 1. Platform Adapters: Adapters (e.g., for Discord, Telegram, or WhatsApp) extend BasePlatformAdapter [1][4]. They are responsible for establishing connections via connect, listening for platform-specific events, and sending messages via send [5][2]. 2. Message Handling: When an adapter receives a message from its respective platform, it constructs a MessageEvent and calls self.handle_message(event) [2]. This method is provided by the base class and routes the event to the gateway's core runner, which manages further processing and interaction with the agent [2]. 3. Hermes Context: The term self.hermes within your query likely refers to an internal reference to the agent's core runtime or context accessible to the adapter instance. In the context of platform adapters, the base class coordinates these interactions, ensuring that platform-specific transport logic is abstracted from the agent's core behavior [5][2]. For developers creating new adapters, the workflow is to subclass BasePlatformAdapter and utilize self.handle_message to relay incoming events, ensuring zero changes are required to the core Hermes codebase [5][6].

Citations:


🌐 Web query:

Raw GitHub base.py handle_message BasePlatformAdapter "self._hermes"

💡 Result:

The handle_message method within BasePlatformAdapter is a core component of the hermes-agent framework developed by Nous Research [1][2]. It is defined in gateway/platforms/base.py and serves as the primary entry point for processing incoming messages from various platform adapters (e.g., Telegram, Discord, WhatsApp) [1][3]. In this context, self._hermes typically refers to the main agent instance or a primary controller that the platform adapter interacts with to route events, manage agent logic, and facilitate responses [1][4]. The handle_message method is designed for high-concurrency, non-blocking operations [4]. Its core logic includes: 1. Session Management: It builds a session key for the incoming message to determine if an agent interaction is already active [4]. 2. Concurrency Control: To prevent race conditions or overlapping tasks, it uses a synchronization pattern (often involving asyncio.Event) to check active sessions before spawning background processing tasks [4]. 3. Interruption Handling: If a message arrives for an already active session, the method may prioritize it (e.g., merging photo bursts or interrupting the current agent process) rather than spawning a new concurrent task [4][5]. 4. Background Processing: It spawns self._process_message_background as an asynchronous task to handle the actual message execution, ensuring the adapter can return quickly to the caller [4][5]. The framework uses this pattern to maintain responsive messaging interactions while supporting complex, multi-step agent reasoning tasks [4]. Other repositories or frameworks, such as PyTorch Adapt or computeruseprotocol, may have classes named BaseAdapter or PlatformAdapter, but the specific combination of BasePlatformAdapter, handle_message, and self._hermes is unique to the hermes-agent codebase [1][4][6][7].

Citations:


🌐 Web query:

"gateway/platforms/base.py" "__init__" "self._hermes" "BasePlatformAdapter"

💡 Result:

The class BasePlatformAdapter, located in gateway/platforms/base.py within the NousResearch/hermes-agent repository, serves as the base class for platform adapters (e.g., Telegram, Discord, WhatsApp) [1][2][3][4]. Regarding the specific initialization logic and the mention of self._hermes: 1. Initialization: The init method of BasePlatformAdapter takes config (PlatformConfig) and platform (Platform) as arguments [1][3]. It initializes various instance attributes, such as message handlers, session tracking (self._active_sessions, self._session_tasks), and configuration for Auto-TTS and busy text modes [1][3]. 2. self._hermes: In the current version of the codebase, there is no explicit mention or initialization of self._hermes within the BasePlatformAdapter.init method shown in the repository [1][3]. The primary purpose of BasePlatformAdapter is to define an interface for platform-specific logic, including connecting, authenticating, and handling message traffic, rather than interacting directly with a core _hermes object [4][5]. It is possible that self._hermes exists in specific downstream subclasses, or that it is a reference to an earlier or different version of the project not reflected in the current repository structure [1][2]. The documentation for adding a platform adapter confirms that custom adapters should inherit from BasePlatformAdapter and implement methods like connect, disconnect, and send [4][5].

Citations:


🌐 Web query:

site:github.com/nousresearch/hermes-agent "self._process_message_background" "BasePlatformAdapter"

💡 Result:

In the codebase for the NousResearch Hermes Agent, self._process_message_background appears to be an internal method pattern used for handling asynchronous message processing within platform adapters, although it is not a standard member of the public API defined in the BasePlatformAdapter class [1][2][3]. The BasePlatformAdapter serves as the foundational interface for all platform-specific adapters (such as Discord or Telegram) in the Hermes Agent architecture [1][3]. While the base class defines the expected interface and shared utilities, platform-specific implementations often include additional private methods—typically prefixed with an underscore—to manage background tasks, event loops, or asynchronous processing queues specific to that platform's API requirements [3]. If you are encountering this method in a stack trace or code analysis, it is likely part of the internal implementation of a concrete adapter subclass that inherits from BasePlatformAdapter, rather than a method provided by the base class itself [3]. You can verify this by checking the specific adapter implementation (e.g., in gateway/platforms/discord.py or similar files in the project) to see how the background message processing logic is structured [3].

Citations:


🌐 Web query:

github raw "gateway/platforms/base.py" "async def handle_message" "_active_sessions"

💡 Result:

The code references you are looking for originate from the NousResearch/hermes-agent repository, specifically within the file gateway/platforms/base.py [1][2][3]. In the BasePlatformAdapter class defined in this file, the _active_sessions attribute and logic related to handle_message are used to manage the state of active user sessions [3]. Specifically, the code utilizes _active_sessions to track concurrent requests or sessions to prevent conflicts—such as preventing an old task from accidentally deleting the guard (busy state) of a newer task—and to handle tasks like stopping processes or managing session-level execution [3]. The handle_message function is a foundational part of the platform adapter interface, designed to be implemented by subclasses to process incoming messages from various platforms (e.g., Telegram, Discord, WhatsApp) [1][2]. The logic within these platform adapters typically includes: - Managing asynchronous message reception and response generation [1][2][3]. - Utilizing session-specific state tracking to ensure that the bot does not attempt to process multiple requests for the same session concurrently if that would lead to state corruption or race conditions [3]. - Implementing mechanisms to track background tasks and handle per-chat configurations (such as auto-TTS settings) [2][3]. If you are exploring this for development purposes, please note that this is part of a specific agentic framework (hermes-agent) and the implementation details are tightly coupled with the system's broader requirement for thread-safe or task-safe session management [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Try to inspect the actual installed package/source paths without changing repo files.
python3 - <<'PY'
import importlib.util, inspect
modpath = importlib.util.find_spec('gateway.platforms.base')
print('gateway.platforms.base spec:', modpath)
if modpath:
    spec = importlib.util.find_spec('gateway.platforms.base')
    mod = __import__(spec.name)
    if hasattr(mod, 'BasePlatformAdapter'):
        print(inspect.getsourcefile(mod.BasePlatformAdapter))
        try:
            print('handle_message source:\n' + inspect.getsource(mod.BasePlatformAdapter.handle_message))
        except Exception as e:
            print('source error:', type(e).__name__, e)
PY

Repository: eimexdev/t3agent

Length of output: 405


Keep the idempotency lock out of long-running ingress work.

_run_once holds self._idempotency_lock while awaiting operation(). For message.submit, the bypass path awaits self._message_handler(event) inline, and /interrupt awaits cancel_session_processing(...), so a slow command or cancellation path can block urgent incoming requests behind the same single lock. Claim the pending state under the lock, release it before running operation(), then re-acquire only to record the terminal result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/hermes/t3agent/adapter.py` around lines 749 - 809, Update
_run_once so self._idempotency_lock is held only while checking cached requests
and durably recording the pending claim; release it before awaiting operation().
After operation() completes or fails, re-acquire the lock to record and persist
the terminal completed/failed state, preserving duplicate handling and cache
updates without blocking urgent ingress work during _message_handler or
cancel_session_processing.

Comment on lines +1128 to +1132
self._message_destinations[message_id] = {
"chatId": str(chat_id),
**({"threadId": thread_id} if thread_id else {}),
**({"sourceMessageId": source_message_id} if source_message_id else {}),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

_message_destinations grows without bound.

Entries are inserted on every send/edit but never evicted, and _message_destinations is a plain Dict. On a long-lived gateway this accumulates one entry per delivered message indefinitely, leaking memory. _processing_sources is cleaned up in on_processing_complete, but this map has no equivalent bound.

Bound it the same way _completed_requests is capped.

🛡️ Proposed fix (declare as OrderedDict + evict)

At the declaration (Line 373):

-        self._message_destinations: Dict[str, Dict[str, str]] = {}
+        self._message_destinations: "OrderedDict[str, Dict[str, str]]" = OrderedDict()

In send after the assignment:

         self._message_destinations[message_id] = {
             "chatId": str(chat_id),
             **({"threadId": thread_id} if thread_id else {}),
             **({"sourceMessageId": source_message_id} if source_message_id else {}),
         }
+        self._message_destinations.move_to_end(message_id)
+        while len(self._message_destinations) > DEFAULT_IDEMPOTENCY_CACHE_SIZE:
+            self._message_destinations.popitem(last=False)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self._message_destinations[message_id] = {
"chatId": str(chat_id),
**({"threadId": thread_id} if thread_id else {}),
**({"sourceMessageId": source_message_id} if source_message_id else {}),
}
self._message_destinations[message_id] = {
"chatId": str(chat_id),
**({"threadId": thread_id} if thread_id else {}),
**({"sourceMessageId": source_message_id} if source_message_id else {}),
}
self._message_destinations.move_to_end(message_id)
while len(self._message_destinations) > DEFAULT_IDEMPOTENCY_CACHE_SIZE:
self._message_destinations.popitem(last=False)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/hermes/t3agent/adapter.py` around lines 1128 - 1132, Bound
_message_destinations using the same capped OrderedDict strategy as
_completed_requests: update its declaration to the appropriate ordered, bounded
mapping and evict the oldest entry immediately after inserting a new destination
in the send/edit flow. Preserve the existing destination data and eviction limit
used by _completed_requests.

@eimexdev

Copy link
Copy Markdown
Owner Author

Follow-up integration verification (2026-07-22):

  • Added the T3 Agent product mode for the initial Hermes-only surface: T3 Agent branding, explicit Hermes · Connected status, flat Conversations navigation, hidden project/CWD/git/PR/build controls, a read-only Hermes model label, and Hermes-only thread/provider filtering.
  • Corrected the live same-machine bridge callback address so Hermes can return events to the T3 Agent server while the browser connects over Tailscale.
  • Verified a real controlled-browser round trip: sent Reply with exactly: Hermes round trip works. from T3 Agent, Hermes completed in 6.7s, the response rendered in the same thread, and the turn closed normally.
  • Focused verification: 20 web unit tests passed; targeted web typecheck, lint, formatting, production build, and integrated browser verification passed.
  • The hidden compatibility project is auto-bootstrapped for the current server and is not exposed in the conversation flow. Sidebar V2 remains a later product-surface pass, matching the V1 scope decision.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/Sidebar.tsx`:
- Around line 2890-2898: Update the Sidebar project selection so it derives the
exact Agent compatibility project instead of relying on sortedProjects[0]. Use
that same filtered Agent-only project list or project value for both rendering
conversation sections and handleCreateT3AgentThread, ensuring unrelated projects
cannot create empty sections or receive new conversations.
- Around line 1196-1199: Update the visibleSidebarThreadKeys computation to
apply the same IS_T3_AGENT_MODE forced-expansion rule as projectExpanded, rather
than relying solely on persisted resolveProjectExpanded state; ensure Hermes
threads from agent-forced-open projects remain included for jump hints, keyboard
traversal, and prewarming.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0bf90027-bc09-4993-8764-de81b54e3e87

📥 Commits

Reviewing files that changed from the base of the PR and between 566b963 and f3c1b06.

📒 Files selected for processing (14)
  • apps/web/src/branding.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/components/chat/ChatComposer.tsx
  • apps/web/src/components/chat/ChatHeader.tsx
  • apps/web/src/components/chat/DraftHeroHeadline.tsx
  • apps/web/src/components/settings/SettingsPanels.tsx
  • apps/web/src/components/settings/SettingsSidebarNav.tsx
  • apps/web/src/components/sidebar/SidebarChrome.tsx
  • apps/web/src/productMode.test.ts
  • apps/web/src/productMode.ts
  • apps/web/src/routes/__root.tsx
  • integrations/hermes/t3agent/__init__.py
  • integrations/hermes/t3agent/tests/test_adapter.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • integrations/hermes/t3agent/tests/test_adapter.py

Comment on lines +1196 to +1199
const storedProjectExpanded = useUiStateStore((state) =>
resolveProjectExpanded(state.projectExpandedById, projectPreferenceKeys),
);
const projectExpanded = IS_T3_AGENT_MODE || storedProjectExpanded;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep sidebar keyboard visibility aligned with forced expansion.

Agent mode forces these project panels open, but visibleSidebarThreadKeys still uses persisted resolveProjectExpanded state. A previously collapsed project can therefore render its Hermes threads while omitting them from jump hints, keyboard traversal, and prewarming. Apply the same Agent-mode expansion rule there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/Sidebar.tsx` around lines 1196 - 1199, Update the
visibleSidebarThreadKeys computation to apply the same IS_T3_AGENT_MODE
forced-expansion rule as projectExpanded, rather than relying solely on
persisted resolveProjectExpanded state; ensure Hermes threads from
agent-forced-open projects remain included for jump hints, keyboard traversal,
and prewarming.

Comment on lines +2890 to +2898
const t3AgentCompatibilityProject = sortedProjects[0]?.memberProjects[0] ?? null;
const handleCreateT3AgentThread = useCallback(() => {
if (!t3AgentCompatibilityProject) {
return;
}
void handleNewThread(
scopeProjectRef(t3AgentCompatibilityProject.environmentId, t3AgentCompatibilityProject.id),
);
}, [handleNewThread, t3AgentCompatibilityProject]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Select and render only the Agent compatibility project.

sortedProjects[0] is user-sort/grouping dependent, so “New conversation” can target an unrelated project. The same unfiltered list is rendered while only its threads are filtered, leaving non-Hermes projects able to produce empty conversation sections. Derive the exact compatibility project (or an Agent-only project list) and use it for both rendering and creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/Sidebar.tsx` around lines 2890 - 2898, Update the
Sidebar project selection so it derives the exact Agent compatibility project
instead of relying on sortedProjects[0]. Use that same filtered Agent-only
project list or project value for both rendering conversation sections and
handleCreateT3AgentThread, ensuring unrelated projects cannot create empty
sections or receive new conversations.

@eimexdev
eimexdev merged commit f3c1b06 into main Jul 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant