Skip to content

feat(proactive): Proactive Communication Loop + BartokGraph (clean extract of #1)#3

Merged
Bartok9 merged 1 commit into
mainfrom
feat/proactive-communication-loop-clean
May 30, 2026
Merged

feat(proactive): Proactive Communication Loop + BartokGraph (clean extract of #1)#3
Bartok9 merged 1 commit into
mainfrom
feat/proactive-communication-loop-clean

Conversation

@Bartok9

@Bartok9 Bartok9 commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

A focused, reviewable extraction of the Proactive Communication Loop subsystem from PR #1 (feat/proactive-communication-loop-v2), which bundled this feature with ~466 unrelated files and had failing CI. This PR contains only the feature + its tests, rebased on current main.

The feature lets Hermes traverse an on-device weighted knowledge graph (BartokGraph) built from the user's history and surface non-obvious cross-time/cross-domain connections — opt-in, silent by default, daily rate-limited.

What's included (12 files)

  • hermes_cli/bartokgraph.py — three-layer (KNOWLEDGE / CODE / PERSON) on-device knowledge graph. Credential redaction, file-type weighting. last_seen_ts uses each source file's mtime, not build time (the original bug: every node got the build timestamp, so the loop saw everything as "active in the last 24h" and surfaced zero connections).
  • hermes_cli/bartokgraph_adapter.py — connection provider bridging the graph to the loop.
  • hermes_cli/proactive_communication_loop.py — synthesis pipeline. Never sends without a graph connection; respects proactive_communication.max_per_day; fails silent.
  • hermes_cli/proactive_scheduler.py — per-session flow scheduler, runs in the gateway cron-ticker thread.
  • plugins/bartokgraph/__init__.py — plugin registration.
  • gateway/run.pyminimal guarded wiring into the existing cron ticker (init once + tick hook). Only ~25 lines; none of the unrelated gateway churn from fix: last_seen_ts should use file mtime not build time (PR #22811) #1.
  • tests/ — graph, loop, scheduler, smoke tests + fixture.
  • docs/features/proactive-communication-loop.md.

Test-robustness fix

The loop tests seeded conversation history with a fixed epoch timestamp. With the loop's 72h history window, that timestamp ages out as wall-clock time advances, so test_temporal_bridge_high_score_sends loaded empty history and failed (and several no-send tests passed trivially for the wrong reason). Switched the seed to a relative recent timestamp so history is always in-window and the real send/no-send paths are exercised.

Verification

  • 78 passedpytest tests/test_proactive_graph.py tests/test_proactive_communication_loop.py tests/test_proactive_scheduler.py tests/test_proactive_smoke.py
  • ruff check clean on all changed files
  • python -m py_compile gateway/run.py + all modules OK
  • No new dependencies → uv.lock / pyproject.toml untouched
  • All external imports (agent.auxiliary_client.get_text_auxiliary_client, hermes_cli.config.{load_config,cfg_get}, hermes_state.SessionDB, cron.scheduler._deliver_result) verified present on main

Relationship to #1

Supersedes the feature portion of #1. Recommend landing this, then closing #1 (or repurposing it for any genuinely-unrelated remaining changes after a rebase). The feature is opt-in: proactive_communication.enabled defaults to false.

Co-authored-by: Cursor cursoragent@cursor.com

Made with Cursor


Note

Medium Risk
Touches gateway cron and unprompted outbound messaging (user-facing, timing-sensitive) and walks local workspace files for graph builds; feature defaults off but judge calls use the configured LLM provider.

Overview
Adds an opt-in Proactive Communication Loop: when enabled, Hermes can send at most one unprompted message per session per day during a learned (or configured) peak hour, only if an on-device BartokGraph finds a strong dormant link to recent work and an auxiliary judge approves it.

New stack: hermes_cli/bartokgraph.py builds a weighted local knowledge graph (prose/code/person layers, credential redaction, god nodes/clusters); bartokgraph_adapter.py scores cross-time links (Jaccard × importance × temporal decay × boosts); proactive_communication_loop.py runs the synthesis pipeline (72h history, graph required, thresholds, proactive_loop_judge); proactive_scheduler.py derives peak hour from ~30 days of messages and dispatches via cron.scheduler._deliver_result. gateway/run.py wires a ProactiveScheduler into the existing cron ticker (every tick, gated internally). Docs, plugin stub, tests (including relative timestamps so the 72h window does not bit-rot), and a smoke fixture round out the change.

Notable fix: graph nodes use source file mtime for last_seen_ts so “dormant in last 24h” filtering works after a fresh build.

Reviewed by Cursor Bugbot for commit eb4daae. Bugbot is set up for automated code reviews on this repo. Configure here.

…n extract)

Extracts the Proactive Communication Loop subsystem from the large
feat/proactive-communication-loop-v2 branch (#1) into a focused,
reviewable change against current main.

Adds:
- hermes_cli/bartokgraph.py — on-device three-layer knowledge graph
  (KNOWLEDGE/CODE/PERSON), credential redaction, file-weighted extraction.
  last_seen_ts uses each source file's mtime (not build time) so the loop
  doesn't treat every node as "recently active".
- hermes_cli/bartokgraph_adapter.py — graph connection provider.
- hermes_cli/proactive_communication_loop.py — synthesis pipeline; silent
  by default, never sends without a graph connection, daily rate-limited.
- hermes_cli/proactive_scheduler.py — per-session flow scheduler, runs in
  the gateway cron ticker thread.
- plugins/bartokgraph/__init__.py — plugin registration.
- gateway/run.py — minimal cron-ticker wiring (guarded, opt-in).
- tests + fixture for graph, loop, scheduler, smoke.

Test robustness: the loop tests seeded conversation history with a fixed
epoch timestamp, which ages out of the 72h history window over time and
caused test_temporal_bridge_high_score_sends to fail (history loaded
empty). Switched to a relative recent timestamp so the seeded history is
always inside the window and the send/no-send paths are actually exercised.

Verified: 78 proactive tests pass; ruff clean; no new dependencies.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown

🔎 Lint report: feat/proactive-communication-loop-clean vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 7746 on HEAD, 7725 on base (🆕 +21)

🆕 New issues (8):

Rule Count
unresolved-import 4
unresolved-attribute 1
no-matching-overload 1
invalid-argument-type 1
not-iterable 1
First entries
tests/test_proactive_communication_loop.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/test_proactive_graph.py:10: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/test_proactive_graph.py:232: [unresolved-attribute] unresolved-attribute: Attribute `weight` is not defined on `None` in union `GraphNode | None`
plugins/bartokgraph/__init__.py:39: [unresolved-import] unresolved-import: Module `hermes_cli.bartokgraph_adapter` has no member `_resolve_local_model_provider`
hermes_cli/proactive_scheduler.py:194: [no-matching-overload] no-matching-overload: No overload of function `max` matches arguments
tests/test_proactive_graph.py:255: [invalid-argument-type] invalid-argument-type: Argument to bound method `KnowledgeGraph.add_edge` is incorrect: Expected `str`, found `str | None`
tests/test_proactive_scheduler.py:11: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
hermes_cli/bartokgraph.py:242: [not-iterable] not-iterable: Object of type `list[Pattern[Unknown]] | None` may not be iterable

✅ Fixed issues: none

Unchanged: 4068 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Plugin imports non-existent function causing ImportError
    • Removed the missing adapter function import and export so the bundled plugin imports successfully.
  • ✅ Fixed: Regex character class matches wrong characters for require
    • Corrected the require capture group to use a negated quote character class.
  • ✅ Fixed: Cluster boost loop iterates but checks same topic
    • Changed the cluster boost loop to derive a topic id from each active topic instead of reusing the best topic.
  • ✅ Fixed: Classify function always returns temporal_bridge for knowledge nodes
    • Removed redundant temporal branches and made recent non-person knowledge nodes classify as cross_domain.

Create PR

Or push these changes by commenting:

@cursor push 64018ec1b1
Preview (64018ec1b1)
diff --git a/hermes_cli/bartokgraph.py b/hermes_cli/bartokgraph.py
--- a/hermes_cli/bartokgraph.py
+++ b/hermes_cli/bartokgraph.py
@@ -480,7 +480,7 @@
 # Matches JS require/ES import strings and Python import/from statements
 _IMPORT_RE = re.compile(
     r"from\s+([\w./][\w./]{2,59})\s+import"
-    r"|require\s*\(\s*['\"]([ ^'\"]{2,59})['\"]{1}\)"
+    r"|require\s*\(\s*['\"]([^'\"]{2,59})['\"]{1}\)"
     r"|import\s+([\w.]{2,59})",
     re.MULTILINE,
 )

diff --git a/hermes_cli/bartokgraph_adapter.py b/hermes_cli/bartokgraph_adapter.py
--- a/hermes_cli/bartokgraph_adapter.py
+++ b/hermes_cli/bartokgraph_adapter.py
@@ -222,8 +222,8 @@
             # Boost for cluster alignment: if today's topic is in the same cluster
             # as this dormant node, that's structurally significant
             cluster_boost = 1.0
-            for tokens in topic_tokens:
-                topic_id = _to_node_id(best_topic)
+            for topic in active_topics[:8]:
+                topic_id = _to_node_id(topic)
                 if topic_id in self._cluster_map and node.id in self._cluster_map:
                     if self._cluster_map[topic_id] == self._cluster_map[node.id]:
                         cluster_boost = 1.3
@@ -317,11 +317,9 @@
         return "person_knowledge"
     if node.layer == "code":
         return "cross_domain"
-    if is_god and days_apart >= 7:
-        return "temporal_bridge"
     if days_apart >= 7:
         return "temporal_bridge"
-    return "temporal_bridge"
+    return "cross_domain"
 
 
 def _explain(topic: str, node, conn_type: str, days_apart: int, importance: float, is_god: bool) -> str:

diff --git a/plugins/bartokgraph/__init__.py b/plugins/bartokgraph/__init__.py
--- a/plugins/bartokgraph/__init__.py
+++ b/plugins/bartokgraph/__init__.py
@@ -36,9 +36,9 @@
         rebuild_interval_days: 7
 """
 
-from hermes_cli.bartokgraph_adapter import BartokGraphAdapter, _resolve_local_model_provider
+from hermes_cli.bartokgraph_adapter import BartokGraphAdapter
 
-__all__ = ["BartokGraphAdapter", "_resolve_local_model_provider"]
+__all__ = ["BartokGraphAdapter"]
 
 PLUGIN_NAME = "bartokgraph"
 PLUGIN_VERSION = "1.0.0"

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit eb4daae. Configure here.

rebuild_interval_days: 7
"""

from hermes_cli.bartokgraph_adapter import BartokGraphAdapter, _resolve_local_model_provider

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Plugin imports non-existent function causing ImportError

High Severity

plugins/bartokgraph/__init__.py imports _resolve_local_model_provider from hermes_cli.bartokgraph_adapter, but this function does not exist anywhere in the codebase. Loading this plugin will raise an ImportError at runtime.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eb4daae. Configure here.

Comment thread hermes_cli/bartokgraph.py
# Matches JS require/ES import strings and Python import/from statements
_IMPORT_RE = re.compile(
r"from\s+([\w./][\w./]{2,59})\s+import"
r"|require\s*\(\s*['\"]([ ^'\"]{2,59})['\"]{1}\)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Regex character class matches wrong characters for require

Medium Severity

The require regex capture group uses ([ ^'\"]{2,59}) which is a character class matching space, caret, and quotes literally. The intent was ([^'\"]{2,59}) — a negated class matching anything except quotes. The leading space turns ^ from a negation operator into a literal character.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eb4daae. Configure here.

if topic_id in self._cluster_map and node.id in self._cluster_map:
if self._cluster_map[topic_id] == self._cluster_map[node.id]:
cluster_boost = 1.3
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cluster boost loop iterates but checks same topic

Low Severity

The loop for tokens in topic_tokens is intended to check cluster alignment across all active topics, but the body always computes topic_id = _to_node_id(best_topic) — a value that doesn't change per iteration. The loop variable tokens is never used, making the loop evaluate the identical condition repeatedly with no effect.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eb4daae. Configure here.

return "temporal_bridge"
if days_apart >= 7:
return "temporal_bridge"
return "temporal_bridge"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Classify function always returns temporal_bridge for knowledge nodes

Low Severity

_classify has three conditional branches for non-person, non-code nodes that all return "temporal_bridge" — including the final fallback for days_apart < 7. The is_god check and the days_apart >= 7 check are redundant since they produce the same output. The cross_domain type is unreachable for knowledge-layer nodes, contradicting the documented three-type classification.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eb4daae. Configure here.

@Bartok9

Bartok9 commented May 30, 2026

Copy link
Copy Markdown
Owner Author

CI note

Green except the test job, whose failures are pre-existing on main and unrelated to this PR:

  • This branch adds/touches none of the 41 failing test files (all in tests/gateway/*, tests/tools/*, tests/hermes_cli/{test_model_*,test_tencent_*,test_update_yes_*}, tests/run_agent/*). My change is confined to the bartokgraph/proactive subsystem + a guarded gateway hook.
  • All 78 proactive/bartokgraph tests pass (within the 20,980 passing).
  • The failures are stale assertions + environment deps in the runner — e.g. test_hy3_preview_context_length asserts 256000 but agent.model_metadata returns 262144 (reproduces on a clean main), and the test_browser_chromium_check / sandbox tests need browser binaries / optional modules (websockets, fal_client) absent from the runner.
  • This PR also fixes three checks that were red on fix: last_seen_ts should use file mtime not build time (PR #22811) #1: e2e, Scan PR for critical supply chain risks, and uv lock --check all pass here.

Net: no regressions introduced; the subsystem is green. The test redness is a separate, pre-existing main-side issue.

@Bartok9 Bartok9 merged commit 60a7524 into main May 30, 2026
8 of 9 checks passed
@Bartok9 Bartok9 deleted the feat/proactive-communication-loop-clean branch May 30, 2026 05:04
Bartok9 added a commit that referenced this pull request May 31, 2026
A bare `/resume` printed the recent-sessions list but armed no selection
state, so typing just `3` on the next line was sent to the agent as chat
instead of resuming session #3. `/resume 3` worked, but the natural
list-then-pick flow did not.

Arm a one-shot pending-resume prompt when bare `/resume` shows the list,
and consume the next bare numeric input as the selection (out-of-range is
reported, non-numeric/other commands disarm it). Resolves against the same
_list_recent_sessions(limit=10) list used everywhere else.

Closes NousResearch#34584.
Bartok9 added a commit that referenced this pull request Jun 6, 2026
…NousResearch#34192) (NousResearch#34382)

NousResearch#34192 reports Hostinger's 'Hermes WebUI' catalog crashes on startup
with:

  /usr/bin/tini: No such file or directory

The image moved from tini to s6-overlay as PID 1 (/init) earlier in
2026. Orchestration templates that still pin /usr/bin/tini as the
entrypoint \u2014 like the Hostinger Hermes WebUI catalog \u2014 have no
binary to exec and the container crashes immediately.

Hermes has no control over the Hostinger catalog template, but we can
make the image backward-compatible by symlinking /usr/bin/tini -> /init
during the s6-overlay install step. External wrappers that exec
/usr/bin/tini will land on the same s6-overlay reaper they would have
landed on if they'd used the canonical /init entrypoint.

The image's own ENTRYPOINT continues to be /init verbatim \u2014 the shim
is purely for legacy external wrappers, not for the image's own
runtime path. Once affected catalogs are updated, the symlink can be
removed.

Other issues NousResearch#34192 raises that are NOT addressed by this PR:

  * Problem #2 (UID 1024 vs 10000 mismatch): already fixed by NousResearch#33148
    (S6_KEEP_ENV=1) and NousResearch#32412 (with-contenv shebangs). The Hostinger
    template likely needs to update its env-var propagation.

  * Problem #3 (incompatible session formats): RFC for pluggable
    SessionDB is tracked in NousResearch#23717.

  * Problem #4 (Telegram polling conflict): an operations problem on
    Hostinger's side, not in this codebase.

This PR is scoped to the one issue that can be fixed inside
Dockerfile: the missing /usr/bin/tini binary.

Tests (3 in test_dockerfile_tini_compat_shim.py):

  - test_tini_compat_symlink_present
    Guard: the symlink line must exist in Dockerfile.
  - test_tini_compat_comment_explains_why
    The NousResearch#34192 anchor comment must be present so future readers know
    why the shim is there (avoid accidental removal).
  - test_entrypoint_still_init_not_tini
    Sanity check: ENTRYPOINT remains /init (s6-overlay). The shim is
    only for external wrappers.

Refs: NousResearch#34192
Partial fix: addresses the immediate tini-binary crash. Catalog-side
fixes still needed by Hostinger for the UID and session-format
problems documented in the issue.

Co-authored-by: Cursor <cursoragent@cursor.com>
Bartok9 added a commit that referenced this pull request Jun 6, 2026
…w goals (NousResearch#34196, NousResearch#34197)

Two related /goal bugs:

(review/reflect/suggest/analyze) unless the assistant uses a magic
phrase like 'goal complete'. The synthetic continuation loop escalates
reflection into producing concrete artifacts that the goal only listed
as *examples* of possible help.

untracked` even when the user did not ask for staging/commit/push.
This races with preflight compression and survives session split,
turning a scoped 'done' answer into out-of-scope artifact production.

Both bugs converge on the goal-judge prompt machinery in
`hermes_cli/goals.py`. The fix is layered, minimal, and reviewable:

1. Tighten JUDGE_SYSTEM_PROMPT with three new explicit guardrails:
   - EXPLORATORY goals (review/reflect/suggest/analyze) are completable
     by a substantive synthesis — do NOT require additional artifacts.
   - Do NOT infer incompletion from untracked / unstaged / uncommitted
     files unless the goal explicitly required staging/commit/push.
   - Do NOT require a magic phrase like 'goal complete'.
   - Treat 'for example' / 'maybe' / 'you could' items as illustrative,
     NOT as required deliverables.
   - Scope-narrow goals (one file, one section, one specific change)
     are DONE when that exact scope is confirmed done — do not expand.

2. Add a transparent keyword classifier `_classify_goal_shape(goal)`
   that returns 'exploratory', 'illustrative', or 'concrete'. Cheap,
   reviewer-friendly substring detection — the LLM judge still makes
   the final DONE/CONTINUE call, but it now sees what kind of goal it
   is. Kept intentionally simple so behaviour is easy to audit and
   tune from issue feedback.

3. Append a corresponding goal-shape hint to the user-prompt template
   when the goal is exploratory or illustrative. The hint reminds the
   judge that for those shapes, a high-quality synthesis IS the
   deliverable. Concrete goals get the original strict template
   unchanged.

4. The with-subgoals template (already enforces strict per-criterion
   evidence) deliberately does NOT receive the shape hint — the
   user's explicit /subgoal criteria take precedence over goal-shape
   heuristics.

Why prompt-level vs adding new state:

This intentionally avoids adding new GoalState fields, new gateway
plumbing, or new compression-lifecycle coupling. The goal judge is the
single point where 'should we continue?' is decided; teaching it to
read goal shape correctly is the smallest change that addresses both
issues' root cause without touching the compression race window
described in NousResearch#34197 #2-#5. Those lifecycle concerns are real and
documented in the issue's 'Proposed fixes #3-#6' — they belong in a
separate gateway-side change. This PR fixes the judge's bad
'continue' verdict that triggers the lifecycle problem in the first
place. Without the bad verdict, the race window in NousResearch#34197 has nothing
to race over.

Tests (17 new, all 67 in test_goals.py pass):
- TestClassifyGoalShape (9 tests): exploratory/illustrative/concrete
  classification including the sanitized NousResearch#34196 repro goal text.
- TestJudgeSystemPromptGuardrails (4 tests): system prompt mentions
  exploratory goals, warns about untracked files, warns about
  requiring magic phrases, warns about illustrative examples.
- TestJudgePromptIncludesGoalShapeHint (4 tests): the user prompt
  receives the shape hint for exploratory/illustrative goals, does
  NOT for concrete goals, and the with-subgoals template skips the
  hint to preserve its strict per-criterion evidence rule.

Refs: NousResearch#34196 NousResearch#34197
Closes: NousResearch#34196 NousResearch#34197

Co-authored-by: Cursor <cursoragent@cursor.com>
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