Skip to content

fix(email): make Outlook/Gmail mail show up in the email app - #4

Merged
vjvarada merged 3 commits into
mainfrom
fix/email-outlook-sync-folders
Jun 20, 2026
Merged

fix(email): make Outlook/Gmail mail show up in the email app#4
vjvarada merged 3 commits into
mainfrom
fix/email-outlook-sync-folders

Conversation

@vjvarada

Copy link
Copy Markdown
Contributor

Why

User connected Microsoft Outlook but no mail appeared in the email app. Diagnosed against the live VPS — the mail was actually synced (200 messages in email_messages) but three independent bugs sat between the database and the screen, plus two that would break things shortly after.

Bugs fixed (all confirmed against production)

  1. Doubled API path → 404. The email app's gatewayFetch prefixed /api/email while every caller also passed /email/..., producing /api/email/email/accounts → gateway /email/email/accounts404. Gateway logs showed this repeating. Now prefixes /api.
  2. Folder mismatch → empty inbox. Providers stored opaque provider folder IDs (Outlook parentFolderId, Gmail labelIds[0]) while the UI/gateway query WHERE folder='inbox'. Added canonical_folder() and normalize on sync; gateway folder filter is now case-insensitive and treats starred as a flag.
  3. Background sync never started. start_background_sync() used structlog-style kwargs on a stdlib logger → TypeError: Logger._log() got an unexpected keyword argument 'accounts' (seen in journal as gateway.email_sync_skipped). Converted scheduler logging to %s.
  4. Token refresh impossible. OAuth callback stored tokens without client_id/client_secret/tenant_id. Now persisted with the token blob, plus write-back of rotated tokens after sync/folder calls.
  5. Gmail received_at always None (datetime.timezone used on the datetime class).

Post-merge steps

  • CI auto-deploys (restarts acb-gateway, rebuilds workbench).
  • Reconnect the Outlook mailbox so the stored credentials include the app client id/secret (old row predates fix fix(email): make Outlook/Gmail mail show up in the email app #4). Next sync rewrites folders canonically and mail appears.
  • Stale anonymous duplicate account already removed from prod DB.

Tests

  • tests/unit/test_email_folders.py (folder normalization) — passing.
  • All changed Python files byte-compile.

🤖 Generated with Claude Code

vjvarada and others added 3 commits June 19, 2026 17:16
…time

- Fix config.json fields to match what the system actually consumes (add tool_scope, icon, category, webhook_routes; mark model_tier/execution_budget/authority as not consumed)
- Add §2a: Platform-Injected Tools catalog (15+ tools agents get for free)
- Add §2b: tool_scope documentation (Berkeley leaderboard accuracy fix)
- Fix naming convention: bare slug in config.json, no agent- prefix
- Add §18: Agent Registration Flow (step-by-step walkthrough)
- Add Key files table explaining system.md vs instructions.md vs .agent.md
- Expand §17: mutation sandbox Docker process, auto-push gate, human review queue
- Update build checklist to 22 items, add 3 anti-patterns
- Clarify github-copilot vs maf runtime modes and how agent_runtime is auto-set
Five bugs, all confirmed against production data, prevented connected
mailboxes from displaying any mail:

1. Doubled API path: the email app's gatewayFetch prefixed `/api/email`
   while callers also passed `/email/...`, so every request hit
   `/api/email/email/...` and 404'd. Prefix with `/api` instead.

2. Folder mismatch: providers persisted opaque provider folder IDs
   (Outlook parentFolderId, Gmail labelIds[0]) while the UI/gateway query
   `WHERE folder = 'inbox'`. Added canonical_folder() and normalize on
   sync; gateway folder filter is now case-insensitive + handles starred.

3. Background sync never started: scheduler used structlog-style kwargs on
   a stdlib logger, raising TypeError in start_background_sync(). Converted
   to %s formatting.

4. Token refresh impossible: the OAuth callback stored tokens without the
   client_id/secret/tenant needed to refresh. Persist app creds with the
   token blob, and write back rotated tokens after sync/folder calls.

5. Gmail received_at always None (datetime.timezone on the datetime class).

Adds tests/unit/test_email_folders.py for the folder normalization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vjvarada
vjvarada merged commit 9c9d0b4 into main Jun 20, 2026
2 checks passed
vjvarada added a commit that referenced this pull request Jul 3, 2026
Ships observability Phases 1+2 (module map review #4, specs/observability_e2.md)
so "error X happened with agent Y" is debuggable after the fact. E2 C+ -> B+.

The gap: logs were uncorrelated colored-console text (couldn't grep by
run/agent) and the full run trace lived only 1h in Redis (latest-run-only),
with the exception message but NO traceback anywhere durable.

Phase 1 - correlated, JSON-able logs (packages/acb_common/_log.py):
  - bind_run_context / clear_run_context / get_run_context bind
    run_id/thread_id/agent/user into structlog contextvars. The executor binds
    them at the run_agent_stream boundary (clears in finally), so EVERY log
    line the run emits - incl. acb_llm.usage, which passes no ids - is
    auto-correlated via the merge_contextvars processor.
  - LOG_FORMAT=json (or json_logs=) switches ConsoleRenderer -> JSONRenderer;
    prod enables it via the systemd EnvironmentFile, no code change.
  - LLM usage audit rows now carry the run context + an agent:<name> actor.

Phase 2 - durable per-run trace store:
  - Migration 50 adds the agent_run table (metadata, status, tokens, duration,
    tool_summary, error_message/type/traceback, trace JSONB, flagged), indexed
    for the diagnostics queries (by agent, status, thread, time; partial on
    status='error').
  - gateway/run_trace.py: build_run_trace_row (pure, unit-tested) derives
    status from the events + a tool summary; record_run_trace upserts it,
    wired into chat_fold.persist_final_assistant_message (reuses the run-end
    Redis replay). Retention: metadata + tool summary for ALL runs; the full
    trace + traceback ONLY for errored/cancelled/flagged runs (storage +
    privacy).
  - Traceback capture: the executor run-error handler logs exc_info=True and
    the agent_run_error audit payload now includes error_type + traceback.

Debug workflow (SSH): psql agent_run WHERE agent_name=Y AND status='error';
journalctl -u acb-gateway -o cat | grep the agent (JSON mode) for the stack.

Designed-not-built (in the spec): Phase 3 /debug/runs diagnostics API, Phase 4
one-command VPS feature-test harness (builds on tests/integration/
test_chat_features.py).

Tests: tests/unit/test_observability.py (11). Migration validated idempotent
against the live DB + an E2E write/read-back. Full suite 664 green, zero
regressions. schema.generated.sql regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vjvarada pushed a commit that referenced this pull request Jul 12, 2026
…e (BO-1)

The Action Broker — the one component allowed to write back to source systems
(AGENTS.md non-negotiable #4) — was a 46-line stub whose propose() only wrote an
audit row. Turn it into a real, tested, NON-BREAKING core (nothing imported it;
it ships with zero handlers so it still cannot perform any real write):

- decide_disposition(authority, destructive): the pure authority-tier policy.
  READ -> rejected; AUTONOMOUS -> auto-apply; SUGGEST -> needs-approval;
  SUGGEST_APPLY -> auto-apply for reversible actions, needs-approval for
  destructive/outward ones (FAIL CLOSED, per the harness rule).
- propose() now computes + audits the disposition (destructive=True default so
  an un-annotated action fails closed).
- Executor registry: register_action_handler / execute. A real provider write
  happens ONLY inside a registered handler; an action with no handler is REFUSED
  (never silently applied); rejected proposals never execute. All audited.
- 8 unit tests (policy matrix + fail-closed executor).

Still pending (needs per-agent authority decisions + a queue table, tracked in
the checklist): a pending_actions table, approval-inbox binding, real handlers,
and routing the existing ClickUp/email writes through execute().

Full unit suite: 887 passed; trajectory evals: 118 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQmgixG7WncBMumvLNsMz8
vjvarada added a commit that referenced this pull request Jul 13, 2026
Exposes the pending_actions queue over HTTP so an operator (Control Plane
inbox) can review outward-write proposals — the sibling of the self-mutation
approval routes.

  GET  /actions/pending               → the approval queue
  POST /actions/pending/{id}/approve  → execute the registered handler
  POST /actions/pending/{id}/reject   → refuse, never executed

Every endpoint is gated on require_internal_auth (payloads carry outward-write
bodies — never anonymous-reachable). approve/reject delegate to
action_broker.approve/reject, which fail closed. Registered in main.py behind
the same try/except guard as the other routers (a load error can't crash the
gateway). The broker still has zero handlers, so approve() returns "no handler"
until real handlers are wired — the queue is visible + auditable but inert
(non-negotiable #4 intact).

Tests (tests/unit/test_actions_routes.py, 4): handlers delegate to the broker,
reviewer identity threads through (with an "operator" fallback), and every
write route carries require_internal_auth. Verified the full app imports with
all three routes registered.

Remaining under A2: the Next.js proxy + inbox pane, real handler registration,
rerouting the bypassing ClickUp/email writes through submit() (needs B1), and
live-Postgres integration verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vjvarada added a commit that referenced this pull request Jul 13, 2026
Makes the broker the single audited chokepoint for the ClickUp outward writes
(create_task / update_task / create_project), per non-negotiable #4 — without
changing default behaviour.

These writes are already user-approved (staged sync_state='pending' → the user
pushes), so a new BaseTaskProvider._broker_gate() AUDITS each write via
action_broker.propose() and AUTO-APPLIES it (runs the real write immediately,
result unchanged). Every outward task write now flows through — and is recorded
by — the broker.

- ACTION_BROKER_ENFORCE kill-switch (default off): flip an action (or "all") to
  the approval QUEUE instead — flippable via env + restart, no redeploy. The
  queue path returns a pending marker; executing on approval needs a persistent
  handler (documented follow-up), so it stays off by default.
- Fail-safe: a broker-layer error never blocks a user-approved write (do_write
  still runs). do_write executes exactly once; its own HTTP errors propagate.
- Audit payloads carry only non-secret write metadata (title/status/fields) —
  never the token (handlers use self._headers()).

Tests (tests/unit/test_provider_broker_gate.py, 4): auto-apply default, queue
when enforced, per-action enforce, and fail-safe on broker error. Full non-e2e
subset green (833 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vjvarada
vjvarada deleted the fix/email-outlook-sync-folders branch July 19, 2026 16:43
vjvarada pushed a commit that referenced this pull request Jul 24, 2026
…roker

A one-to-many broadcast ("send this monsoon update to all 18 dealer groups") is
the spec's canonical always-approval outward write. /whatsapp/broadcast resolves
the targets (explicit chats or a whole category, owner-scoped) and routes ONE
Action Broker proposal with SUGGEST authority — which the broker maps to
NEEDS_APPROVAL — so nothing sends until a human approves; the route asserts the
disposition was gated (500s if a future change ever made it auto-apply). The
real sends live only in the registered whatsapp.broadcast handler (broker
non-negotiable #4), one failed recipient never aborts the rest. Handler is
registered at startup alongside the post-sync hooks. action-broker added as an
explicit gateway dep. 4 tests pin the never-auto-apply invariant, handler
registration, route, and request shape. Full WA suite 123 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UuXLzXKbBfbJDypE9V3Ffq
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