hermes - ✅(Solved) Fix Pamela: auto-join marks denied Meet admission as success [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#24826Fetched 2026-05-14 03:51:29
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
labeled ×3cross-referenced ×1

Error Message

  • error: host denied admission
  • no denial/error state joined = bool(status.get("inCall") or status.get("joinedAt")) and not status.get("error") state["attempts"][event_id] = {..., "join_status": "failed", "error": status.get("error")}

Root Cause

Suspected Root Cause

pamela-google-meet-autojoin.py trusts the hermes -p pamela meet join ... process exit code. It does not verify post-join status before writing the event into state["joined"].

Fix Action

Fixed

PR fix notes

PR #25047: fix(google_meet): verify admission before reporting meet_join success (#24826)

Description (problem / solution / changelog)

What does this PR do?

Stops hermes meet join from silently reporting denied / lobby-timeout / failed admissions as success.

Issue #24826 reported that the Pamela calendar auto-join cron recorded a meeting as joined even though the host denied admission, then permanently suppressed retries. Root cause: pm.start() (and therefore hermes meet join) returns ok=True / exit 0 as soon as the bot subprocess spawns — admission to the meeting happens later, and nothing on the success path actually verified it. Every consumer (CLI exit code, agent tool result, user auto-join script) had to re-derive "did the bot make it in?" from a half-dozen interacting fields (inCall, joinedAt, error, leaveReason, process liveness), and at least one consumer got it wrong.

This PR centralises the admission verdict in one place and surfaces it through every layer:

  • A new _classify_admission() truth table maps raw bot status to a single admissionState (joined / waiting / denied / lobby_timeout / failed) plus a derived joined: bool.
  • pm.status() now returns those derived fields alongside (and after) the raw bot fields, so a single field check replaces the old re-derivation.
  • A new pm.wait_for_join(timeout=…) blocks until the bot reports a terminal admission state or the wait budget expires; sleep and now are injected so tests run in milliseconds.
  • hermes meet join blocks by default (90s, override via --wait <s>, opt out via --no-wait) and translates the verdict into an actionable exit-code table — denied admission is exit 3, lobby-timeout is 4, etc. Cron / auto-join scripts that trust the exit code can no longer treat a denial as success.
  • The meet_join agent tool gains an opt-in wait_for_admission (default off, preserves v1 async behaviour) so synchronous workflows can branch on the verdict in the same turn.

Related Issue

Fixes #24826

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • plugins/google_meet/process_manager.py — add _classify_admission() truth table, DENIAL_LEAVE_REASONS frozenset (pinned against silent meet_bot renames), wait_for_join() poll helper, and append derived joined / admissionState / normalised error fields to status(). Pure additions — every existing field is preserved, every existing call site keeps working.
  • plugins/google_meet/cli.pyhermes meet join now blocks on wait_for_join by default, prints the full JSON verdict, and maps waitOutcome → exit code (0 joined, 3 denied, 4 lobby_timeout, 5 failed/no_active, 6 timeout). New --wait <seconds> and --no-wait flags.
  • plugins/google_meet/tools.py — new wait_for_admission (default false) and wait_seconds parameters on meet_join. When true, folds wait_for_join into the tool response so success reflects actual admission, not just spawn. Malformed numeric input is coerced to the default rather than raising.
  • tests/plugins/test_google_meet_admission.py38 new test cases covering: the _classify_admission truth table (9 cases), status() derived fields (4 cases), wait_for_join poll loop with injected fake clock (6 cases), hermes meet join exit-code mapping (8 parametrised cases), CLI sync vs async branches (4 cases), meet_join tool wait_for_admission opt-in (6 cases), and a guard that DENIAL_LEAVE_REASONS matches meet_bot's emissions.
  • plugins/google_meet/README.md — new "Verifying admission (cron / auto-join scripts)" section with the full exit-code table, --wait / --no-wait flags, derived status fields, and the agent-tool opt-in.
  • plugins/google_meet/SKILL.md — teach the agent to either pass wait_for_admission=true or check the new joined / admissionState fields on meet_status. Status-table reordered so derived fields appear first.

How to Test

# 1. New test suite passes
./scripts/run_tests.sh tests/plugins/test_google_meet_admission.py
# expected: 38 passed

# 2. Existing google_meet test suites still pass (no regression)
./scripts/run_tests.sh tests/plugins/test_google_meet_plugin.py \
                      tests/plugins/test_google_meet_audio.py \
                      tests/plugins/test_google_meet_node.py \
                      tests/plugins/test_google_meet_realtime.py
# expected: 113 passed

# 3. CLI exit-code contract — manual reproduction of the bug
#    (with no real meeting; start fakes it via the test seam)
python -c "
from unittest.mock import patch
from plugins.google_meet import cli, process_manager as pm
with patch.object(pm, 'start', return_value={'ok': True, 'pid': 1}), \
     patch.object(pm, 'wait_for_join', return_value={
         'ok': True, 'waitOutcome': 'denied', 'joined': False,
         'admissionState': 'denied', 'error': 'host denied admission',
     }):
    rc = cli._cmd_join(url='https://meet.google.com/abc-defg-hij',
                       guest_name='Hermes Agent', duration=None,
                       headed=False)
print('exit code:', rc)
"
# expected: exit code: 3   (was 0 before the fix)

# 4. status() truth table
python -c "
from plugins.google_meet.process_manager import _classify_admission
print(_classify_admission({'inCall': False, 'leaveReason': 'denied',
                           'error': 'host denied admission'}, alive=False))
"
# expected: {'joined': False, 'admissionState': 'denied', 'error': 'host denied admission'}

For end-to-end verification with a real Meet, run hermes meet join <url> against a meeting where the host either ignores or denies the admit prompt — the command will exit 4 (lobby_timeout) or 3 (denied) instead of 0, and the printed JSON will include "waitOutcome": "denied" / "lobby_timeout", "joined": false, and a populated error string.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run ./scripts/run_tests.sh tests/plugins/test_google_meet_*.py and all 113 + 38 tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 15.6 (darwin 24.6.0)

Documentation & Housekeeping

  • I've updated relevant documentation (plugins/google_meet/README.md, plugins/google_meet/SKILL.md)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no new config keys)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A (no architecture change)
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — google_meet plugin no-ops on Windows by design; macOS unchanged
  • I've updated tool descriptions/schemas if I changed tool behavior — meet_join schema updated with wait_for_admission / wait_seconds

Screenshots / Logs

Before the fix (issue #24826 reproduction):

$ hermes meet join https://meet.google.com/uef-fodt-zjj
{ "ok": true, "pid": 12345, ... }
$ echo $?
0

$ hermes meet status
{
  "alive": false,
  "inCall": false,
  "joinedAt": null,
  "error": "host denied admission",
  "leaveReason": "denied"
}
# cron writes the event into autojoin_state.json as "joined" because exit was 0

After the fix:

$ hermes meet join https://meet.google.com/uef-fodt-zjj
{
  "ok": true,
  "alive": false,
  "inCall": false,
  "joinedAt": null,
  "error": "host denied admission",
  "leaveReason": "denied",
  "joined": false,
  "admissionState": "denied",
  "waitOutcome": "denied"
}
$ echo $?
3
# cron sees nonzero exit + admissionState=denied and skips writing it as joined

Changed files

  • plugins/google_meet/README.md (modified, +40/-0)
  • plugins/google_meet/SKILL.md (modified, +12/-9)
  • plugins/google_meet/cli.py (modified, +74/-2)
  • plugins/google_meet/process_manager.py (modified, +148/-1)
  • plugins/google_meet/tools.py (modified, +37/-0)
  • tests/plugins/test_google_meet_admission.py (added, +618/-0)

Code Example

r = subprocess.run(join_cmd, ...)
status = get_meet_status()
joined = bool(status.get("inCall") or status.get("joinedAt")) and not status.get("error")

if joined:
    state["joined"][event_id] = {..., "join_status": "joined"}
    log success
else:
    state["attempts"][event_id] = {..., "join_status": "failed", "error": status.get("error")}
    do not suppress permanently
    return nonzero or print failure notification
RAW_BUFFERClick to expand / collapse

Bug Description

Pamela calendar Meet auto-join reports success and records an event as joined even when Pamela was not admitted to the meeting.

Live test evidence:

  • Auto-join log says: Pamela auto-joined Google Meet: Test (https://meet.google.com/uef-fodt-zjj)
  • hermes -p pamela meet status shows:
    • alive: false
    • inCall: false
    • joinedAt: null
    • error: host denied admission
    • leaveReason: denied
  • State file records join_exit: 0, so future cron runs skip retrying that event.

Expected Behavior

Pamela should only record a meeting as successfully joined when Meet status confirms:

  • inCall: true or non-null joinedAt
  • no denial/error state

If admission is denied or not completed, the cron should:

  • log failure clearly
  • not mark the event as joined/suppressed forever
  • optionally retry on a short cooldown while event is still active
  • notify Melvin that host admission is required / failed

Actual Behavior

The join command returned exit 0, so the cron wrote the event into autojoin_state.json as joined even though Pamela was denied admission and never entered the call.

Suspected Root Cause

pamela-google-meet-autojoin.py trusts the hermes -p pamela meet join ... process exit code. It does not verify post-join status before writing the event into state["joined"].

Relevant script path: /home/melvin/.hermes/scripts/pamela-google-meet-autojoin.py

Relevant lines:

  • invokes join around line 150
  • records joined state around lines 152-160
  • logs success on returncode 0 around lines 162-165

Proposed Fix

After meet join, call hermes -p pamela meet status and require confirmed joined state before success.

Pseudo-logic:

r = subprocess.run(join_cmd, ...)
status = get_meet_status()
joined = bool(status.get("inCall") or status.get("joinedAt")) and not status.get("error")

if joined:
    state["joined"][event_id] = {..., "join_status": "joined"}
    log success
else:
    state["attempts"][event_id] = {..., "join_status": "failed", "error": status.get("error")}
    do not suppress permanently
    return nonzero or print failure notification

Acceptance Criteria

  • Denied admission does not produce Pamela auto-joined success log.
  • Denied admission does not store the event under joined as successful.
  • Cron can retry or notify while event is active.
  • Status/logs clearly expose denial vs join success.
  • Existing successful join path still works.

Labels

Bug, P1, cron, browser/Meet automation.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING