Skip to content

Ellie continuity bridge (Phase 3, Plan B): conversation_id + session-end notify - #2

Merged
zerocool0133700-lgtm merged 6 commits into
mainfrom
feat/ellie-continuity-bridge
Jun 14, 2026
Merged

Ellie continuity bridge (Phase 3, Plan B): conversation_id + session-end notify#2
zerocool0133700-lgtm merged 6 commits into
mainfrom
feat/ellie-continuity-bridge

Conversation

@zerocool0133700-lgtm

Copy link
Copy Markdown
Owner

Summary

The producer side of Ellie's cross-session continuity loop. Builds on the Phase 1 Ellie sidecar bridge (PR #1, merged) so Hermes drives Ellie's memory layer end-to-end. Plan: docs/plans/2026-06-14-ellie-continuity-bridge-plan-b.md.

  • conversation_id on every /api/turnagent._gateway_session_key or agent.session_id, so Ellie reads the right per-conversation digest.
  • notify_ellie_session_end() — at session end, POSTs the final transcript + a monotonic marker to Ellie's /api/session-end, so Ellie rolls the digest forward and distills durable facts to the Forest. Best-effort, fire-and-forget (Ellie returns 202).
  • Lifecycle wiring — fired from shutdown_memory_provider (the single session-end funnel for CLI, gateway, /reset), backend-gated, fire-once, fully fail-soft.

Design notes

  • marker = epoch-millis at session start (not len(history)): must be monotonic across sessions sharing a channel (so a later session's digest wins Ellie's compare-and-set) yet stable across retries of the same session-end (so Ellie's dedupe blocks double-fire).
  • No self.backend attribute exists — backend is config-driven. The hook reads the same config the forwarder uses, so if turns route to Ellie, session-end fires too.
  • Three layers of fail-soft — a shutdown can never raise because Ellie is unreachable.

Test Plan

  • uv run pytest tests/agent/test_ellie_bridge.py14 passed (Phase 1 + new conversation_id / session_end / fire-once / backend-gate cases)
  • Live loop-closure smoke — PASS (docs/plans/2026-06-14-ellie-continuity-smoke-runbook.md): against an isolated 2/2/smoke Forest scope, a fact stated in session 1 ("deploy on Fridays, never Mondays") was distilled to the Forest at session-end, then surfaced via recall in a later, separate session's turn — Ellie answered "Always deploy on: Friday, Never deploy on: Monday." The smoke also found and fixed a too-tight recall_timeout_ms default on the Ellie side (250→2000ms).

Counterpart

Ellie gateway PR NousResearch#64 (feat/ellie-hermes-memory) implements the consumer side: /api/turn conversation_id, /api/session-end, distill-to-Forest, recall preamble.

🤖 Generated with Claude Code

zerocool0133700-lgtm and others added 6 commits June 14, 2026 18:20
Adds _conversation_id() helper that prefers _gateway_session_key (stable
cross-session key for gateway-routed conversations) and falls back to
session_id, then "hermes-unknown". Includes two new tests covering both
paths.
…gger)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zerocool0133700-lgtm
zerocool0133700-lgtm merged commit 6cbf923 into main Jun 14, 2026
zerocool0133700-lgtm pushed a commit that referenced this pull request Jul 30, 2026
…ch#67140)

The background write guard decided ownership from `isinstance(usage_rec, dict)`,
so a local skill with NO usage record passed. That successful write called
bump_patch(), which created a `created_by: null` record — and the identical
write was refused from then on. "Allowed exactly once, then never" is a race
with our own bookkeeping, not a policy. Reproduced on main: patch #1 succeeds,
patch #2 with the same arguments is refused.

Option B from the issue. Option A (split `session_review` from
`scheduled_curator` and let the session fork patch user-owned skills it
consulted) would widen autonomous write permission onto skills the user owns
with no user present to consent — wrong direction for a no-user-present actor.

- skill_manager_tool: missing and explicit-null records now resolve
  IDENTICALLY, both fail closed. The refusal names the reason and points at
  `hermes curator adopt <name>`.
- background_review: both review prompts told the reviewer to patch any skill
  consulted in the session and claimed pinned skills could be improved, while
  enforcement refused both. Prompts now list pinned, external, and user-owned
  skills as protected, and tell the reviewer to RECOMMEND adoption instead of
  attempting a write that will be refused.
- skill_usage: document that `created_by` is a curator-management policy flag,
  not a provenance claim, and add `is_curator_managed()` so call sites read as
  the question they ask. Field name retained — it is on disk in every
  `.usage.json` and renaming would strand those records.
- curator CLI: `hermes curator list-unmanaged` itemizes unmanaged skills with
  the reason each is unmanaged (completes the NousResearch#67139 spec).

Foreground writes are untouched: a user-directed edit to a user-owned skill
still works, including on pinned skills.

Sibling tests: 9 failures in test_skill_manager_tool.py were fixtures that
created record-less skills to exercise OTHER guards (consolidation-delete,
read-before-write) and relied on ownership falling through. Fixed at the
fixture, since the real curator only ever operates on managed sediment. One
test asserted the old "manually authored" wording; rewritten to assert the
behavior contract instead of the string.

Validation: 274 targeted tests + all 7 background-review files (60 tests) pass.
E2E on a temp HERMES_HOME (30 checks) covers the flip, foreground writes,
adoption unblocking, pin semantics, prompt/enforcement parity, and the new verb.
Each new test sabotage-verified: revert the fix, confirm it goes red.

Fixes NousResearch#67140
zerocool0133700-lgtm pushed a commit that referenced this pull request Jul 30, 2026
…hat tile (NousResearch#71969)

* fix: Branch button is a dead no-op inside a branched chat tile

session-tile.tsx wired onBranchInNewChat to () => undefined for
tiled/branched sessions (nested branching isn't supported there), but
the button in AssistantMessage's action bar rendered unconditionally
regardless of whether a real handler was supplied. The button looked
clickable but silently did nothing, with no visual feedback.

- AssistantMessage now only renders the Branch button when
  onBranchInNewChat is actually provided, matching the existing
  pattern used for onDismissError/onRestoreToMessage.
- session-tile.tsx no longer passes a no-op handler; the prop is
  simply omitted so the button doesn't render in tiles.
- onBranchInNewChat is now optional on ChatViewProps, and the
  latestChatActions passthrough wrapper uses the existing
  latestOptional helper instead of an unconditional call.

* test: assert Branch button visibility matches handler presence

Adds coverage for the bug #2 fix: renders Thread with and without an
onBranchInNewChat handler and asserts the Branch in new chat button
is shown only when a real handler is supplied, hidden otherwise -
covering both the normal open-chat case and the session-tile
(branched chat) case that used to leave a dead, clickable button.
zerocool0133700-lgtm pushed a commit that referenced this pull request Aug 10, 2026
… a broken chat

A completely unconfigured install previously booted into a working-looking
chat (banner showed model 'unknown'), accepted a message, spun ~30s, then
failed with 'Set OPENROUTER_API_KEY' — a provider the user never chose —
and never offered setup.

- HermesCLI.run() now probes provider readiness at startup (TTY only) and
  offers the shared provider picker (hermes model flow, which fronts Quick
  Setup / Nous Portal OAuth) when nothing is configured. Decline is
  respected; picker state re-syncs into the live CLI so the next turn works
  without a restart.
- New silent probe _runtime_credentials_ready(): no printing, no state
  mutation; handles keyless local endpoints and callable bearer providers.
- The empty-api-key error is provider-aware: names the actual resolved
  provider and points at 'hermes model' / 'hermes setup' instead of
  hardcoding OPENROUTER_API_KEY.
- Banner: unconfigured installs render 'no model configured — run /model'
  in red instead of the silent 'unknown' model slug.

Consumer-onboarding audit finding #2 (sev 5), Aug 2026.
zerocool0133700-lgtm pushed a commit that referenced this pull request Aug 10, 2026
A wedged adapter transport (network hang, dead websocket) previously
blocked _check_session_stalls forever: sibling candidates in the same
pass were never evaluated and the watcher stopped ticking. Wrap the
send in asyncio.wait_for (15s); on timeout log a WARNING and do NOT
latch, so the next tick retries. Regression uses a never-resolving fake
adapter and proves the pass completes, a healthy sibling candidate is
still notified in the same pass, and the watcher ticks again
(sabotage-verified against the unbounded send).
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