From 22b207be415d8c90c7e53d0f96e2f074e8dc6844 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 14 May 2026 13:12:46 +0600 Subject: [PATCH] =?UTF-8?q?[2026-05-14]=20kraken-access.md:=20fix=20CF=20t?= =?UTF-8?q?unnel=20config=20(HTTP=E2=86=928642,=20not=20SSH)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- family/how-to/kraken-access.md | 9 +- work/plans/no-topic-routing.md | 74 +++++++++-- .../chat-path-onboarding-tracker-blocking.md | 125 ++++++++++++++++++ 3 files changed, 189 insertions(+), 19 deletions(-) create mode 100644 work/wiki/apple-browsers/chat-path-onboarding-tracker-blocking.md diff --git a/family/how-to/kraken-access.md b/family/how-to/kraken-access.md index 9d1b864c..a1d8b120 100644 --- a/family/how-to/kraken-access.md +++ b/family/how-to/kraken-access.md @@ -18,9 +18,9 @@ Host kraken-ssh StrictHostKeyChecking no ``` -`kraken.qentra.top` — CNAME на CF туннель, используется для Hermes API. +`kraken-ssh` — алиас в `~/.ssh/config`, подключается через VPS `91.207.28.205:2223` → reverse tunnel → `localhost:22` Кракена. -`kraken.qentra.top` — просто алиас. Реально подключается на VPS `91.207.28.205:2223` → tunnel → `localhost:22` Кракена. +`kraken.qentra.top` — CF Tunnel, маршрутизирует HTTP на `localhost:8642` (Hermes API). --- @@ -60,9 +60,8 @@ ssh root@91.207.28.205 "ss -tlnp | grep 2223" ## Cloudflared (для будущих сервисов) -Контейнер `cloudflared` запущен на Кракене (`unless-stopped`), туннель `kraken` активен в CF Zero Trust. -Hostname: `kraken.qentra.top → SSH → localhost:22` (настроен но не используется — SSH идёт через VPS). -Можно добавить другие сервисы (OMV, Portainer) через Public Hostnames в Zero Trust. +Контейнер `cloudflared` запущен на Кракене (`unless-stopped`), туннель `kraken` активен в CF Zero Trust. +Hostname: `kraken.qentra.top → HTTP → localhost:8642` (Hermes API). --- diff --git a/work/plans/no-topic-routing.md b/work/plans/no-topic-routing.md index fcc48f4d..8ac9f007 100644 --- a/work/plans/no-topic-routing.md +++ b/work/plans/no-topic-routing.md @@ -1,8 +1,8 @@ # Plan: (no topic) Thread Routing **Date**: 2026-05-14 -**Status**: Planning -**Priority**: Medium +**Status**: In Progress — plugin being implemented +**Priority**: High ## Problem @@ -59,20 +59,66 @@ Eagle may have multiple instances running in different Zulip topics simultaneous Each Eagle instance should know its own Zulip topic context. When spawned in a topic, it registers itself in `zulip_thread_locks`. Eagle reads this to avoid double-posting. +## Implementation + +### Mechanism: `pre_gateway_dispatch` plugin + +Hermes plugins support a `pre_gateway_dispatch` hook that fires **before** auth +and agent dispatch, can intercept, rewrite, or skip any incoming `MessageEvent`. + +Plugin location: `~/.hermes/plugins/zulip-topic-routing/` + +In Zulip adapter (`gateway/platforms/zulip.py`): +- `event.source.thread_id` = Zulip topic name (set at line ~610) +- `event.source.chat_id` = `"stream_name::topic_name"` +- `(no topic)` messages have `thread_id == "(no topic)"` + +### Auth for Zulip API calls + +From `~/.hermes/.env`: +``` +ZULIP_URL=https://zulip.qentra.top +ZULIP_BOT_EMAIL=eagle-bot@zulip.local +ZULIP_API_KEY=BT7zzT...XqAE +``` + +Topic rename API (confirmed working via curl in prior session): +``` +PATCH /api/v1/messages/{message_id}?propagate_mode=change_all&topic={new_topic} +``` + +Create message in topic (effectively creates topic): +``` +POST /api/v1/messages type=stream to=stream_name topic=new_topic content=... +``` + +### Plugin Flow + +1. `pre_gateway_dispatch` fires → check `event.source.thread_id == "(no topic)"` +2. Get stream name from `chat_id.split("::")[0]` +3. Query Zulip API: `GET /api/v1/messages?narrow=[{"operator":"stream","operand":""}]&num_before=0&num_after=20&anchor=newest` +4. Filter out `(no topic)` messages → get most recent topic name + last N messages +5. LLM relevance check (fast, cheap): is `event.text` related to recent topic context? +6. If **related** → rewrite event, prepend `[routed from (no topic)]`, change `event.source.thread_id` +7. If **not related** → rename `(no topic)` thread → descriptive 2-5 word topic, return `allow` + +### Multi-instance Coordination + +Dropped for v1 — single Eagle instance in practice. Can add mutex later if needed. + ## Implementation Steps -1. Add Zulip stream message history query to Eagle's toolkit (REST API call via hermes) -2. Create `zulip_thread_locks` table in `personal_os` DB -3. Write topic routing logic as a function in the Eagle prompt or as a cron-triggered script -4. Test: send message in `(no topic)`, verify routing + rename behavior - -## Questions - -- Does Eagle get invoked *automatically* for every (no topic) message, or only when mentioned? -- Is Eagle a single process or multiple instances per stream? (affects coordination complexity) -- What's the stream scope? Only `master`? Or all streams? +- [x] Confirmed curl approach works for topic creation +- [x] Identified `pre_gateway_dispatch` as correct hook +- [x] Confirmed `thread_id == "(no topic)"` detection pattern +- [ ] Create `~/.hermes/plugins/zulip-topic-routing/plugin.yaml` +- [ ] Create `~/.hermes/plugins/zulip-topic-routing/__init__.py` +- [ ] Enable plugin in `~/.hermes/config.yaml` +- [ ] Test: send `(no topic)` message, verify routing ## Notes -- Current session: this `(no topic)` thread in `master` stream should be renamed to something like `planning: system improvements 2026-05` -- Zulip topic rename API: `PATCH /api/v1/messages/{message_id}` with `topic` param (requires first message ID of the thread) +- Topic rename API requires `propagate_mode=change_all` to rename all messages in thread +- Zulip uses `subject` field internally, `thread_id` in Hermes maps to it +- Free streams setting: `ZULIP_FREE_STREAMS=master,daily-brief,inbox,executor,personal,focus` +- Plugin lives in `~/.hermes/plugins/` (user plugins, override bundled) diff --git a/work/wiki/apple-browsers/chat-path-onboarding-tracker-blocking.md b/work/wiki/apple-browsers/chat-path-onboarding-tracker-blocking.md new file mode 100644 index 00000000..c8817e46 --- /dev/null +++ b/work/wiki/apple-browsers/chat-path-onboarding-tracker-blocking.md @@ -0,0 +1,125 @@ +# iOS: Chat-Path Onboarding — Tracker Blocking Demo + +> **Asana:** [Ship Review task](https://app.asana.com/1/137249556945/task/1214147157456478) +> **Branch stack:** `demo-tracker-blocking-onboarding` → `ui-polish` → `chat-path-dialog-polish` → `sr-feedback` → `uti-flow` +> **Due:** 2026-05-15 + +## Overview + +A/B experiment adding a tracker-blocking demo step to onboarding for users who pick Duck.ai (chat-path). After the Fire Onboarding step, users see: +1. "Try visiting a site!" dialog (address bar + toolbar hidden) +2. Trackers-blocked dialog after a site loads +3. "You've got this!" EOJ completion dialog +4. Subscription upsell ("Oh, before I forget…") + +**Cohorts:** Treatment A (address bar top) · Treatment B (address bar bottom) · Control (no change) +**Audience:** US only · iPhone only · Users who chose Duck.ai in onboarding, in A or B cohort + +--- + +## PR Stack + +| PR | Branch | Status | +|---|---|---| +| [#4544](https://github.com/duckduckgo/apple-browsers/pull/4544) | `demo-tracker-blocking-onboarding` | Open — needs fixes | +| [#4591](https://github.com/duckduckgo/apple-browsers/pull/4591) | `demo-tracker-blocking-onboarding-ui-polish` | Approved — needs rebase | +| [#4664](https://github.com/duckduckgo/apple-browsers/pull/4664) | `demo-tracker-blocking-onboarding-chat-path-dialog-polish` | Open — needs fix | +| [#4668](https://github.com/duckduckgo/apple-browsers/pull/4668) | `demo-tracker-blocking-onboarding-uti-flow` | Open — needs fix | +| [#4855](https://github.com/duckduckgo/apple-browsers/pull/4855) | `demo-tracker-blocking-onboarding-sr-feedback` | Open — needs fixes | + +--- + +## Required Changes (not yet done) + +### PR #4544 — main chat-path branch + +- [ ] **Danger CI**: Replace `@UserDefaultsWrapper(key: .daxChatPathVisitSiteSeen)` with `KeyedStoring` protocol pattern — new `@UserDefaultsWrapper` definitions are blocked by Danger rule. +- [ ] **Mock refactor** (alessandroboron): `MockDaxDialogsSettings.chatPathPhase` should expose `var _chatPathPhase: DaxDialogs.ChatPathPhase = .visitSite` for direct test control, not replicate real computed logic. +- [ ] **Sync restore exclusion**: Exclude users who selected "Restore My Stuff" (sync restore) in `OnboardingIntroViewModel` — Dax dialogs are disabled for those users, breaking the flow. +- [ ] **Pixels**: Coordinate with rachelmcr (PR #4687) — may be able to use `variant` parameter on existing pixels instead of adding new chat-path-specific pixel events. + +### PR #4664 — chat-path dialog polish + +- [ ] **Title scope** (alessandroboron): `RebrandedNewTabDaxDialogFactory` title change ("Next, try visiting a site!") must only apply to chat-path flow, not all onboarding flows. + +### PR #4668 — UTI (Unified Toggle Input) onboarding flow fixes + +Fixes to make the chat-path onboarding dialogs work correctly when the Unified Toggle Input is active: + +- [x] Dialog position: anchor to `coordinator.viewController.view.bottomAnchor` (UTI bar bottom), embed in `unifiedInputContentContainer`. +- [x] Dialog not dismissed on navigation: `viewWillDisappear` removes hosting controller parented to `MainViewController`. +- [x] UTI bar staying active after URL submission: `preserveOmnibarSession` now checks `!tab.isLoading`. +- [x] "You've got this" completion dialog path in UTI mode: `showDuckAIOnboardingCompletionDialogInUTI`. +- [x] UTI bar staying active after completion dismiss: call `dismissUnifiedToggleInputToOmnibar` directly. +- [x] Stale browsing-dialog flags: `setAsChatFirstPath` now resets all browsing-dialog flags to prevent `chatPathPhase` jumping to `.trackerToEOJ` from stale state. +- [ ] **Hardcoded cohort override** (aataraxiaa): `FeatureFlag.swift` has a hardcoded test override — must revert before merge. + +### PR #4591 — ui-polish + +- [x] Approved. Rebase on current base. + +### PR #4855 — sr-feedback + +- [ ] **Hide toolbar + address bar**: During visit-site step, hide both toolbar and address bar via `chromeDelegate?.setBarsHidden(true/false)` — not just lock controls. Costas + Alessandro preference. Simplifies the code too. +- [ ] **Use delegate**: `NewTabPageViewController` should access the parent via `NewTabPageControllerDelegate` instead of `(parent as? MainViewController)?` cast. +- [ ] **Loading guard**: Move `currentTab?.isLoading != true` check into the `if` condition, not a standalone `guard` (avoids orphaned reference and blank NTP on early exit). +- [ ] **Revert Search Experience screen copy**: `searchAndDuckAIOption` was incorrectly changed to `"Toggle between\nSearch and Ask AI"`. Must revert to `"Toggle between\nSearch and Duck.ai"` — the "Ask AI" rename is only for the DuckAIQuery experiment toggle (`DuckAIQueryExperiment.toggleAILabel`). + +--- + +## Privacy Config / Experiment Setup (TBD before shipping) + +- [ ] Rename experiment in privacy config — create new experiment name, disable old one. +- [ ] Set `"targets": [{ "localeLanguage": "en", "localeCountry": "US" }]` in the experiment payload. +- [ ] Inform Pete in [O-N Live Onboarding Experiment Details](https://app.asana.com/1/137249556945/task/1214601039604921) once experiment name is confirmed. + +--- + +## Resolved / Dismissed + +- **GJ6c** ✅ — No longer required. Address bar hidden for visit-site step (GJ3), so user can't trigger a search at that point. Costas confirmed a search after the trackers-blocked dialog is acceptable. +- **GJ3** ✅ — Hide address bar + toolbar for "Try visiting a site" step (chat-path only). +- **GJ2** ✅ — X button removed from chat-path dialogs (PR #4664). +- **GJ4** ✅ — Copy updated to "Next, try visiting a site!" after AI. +- **GJ8** ✅ — "Duck.ai" → "Ask.ai" in DuckAIQuery toggle (onboarding only, not standard toggle). `DuckAIQueryExperiment.toggleAILabel = "Ask.ai"`. +- **"Try fire tabs" flash** ✅ — Suppressed via `isStillOnboarding()` in `DaxDialogs` for chat-path users. +- **GJ7 (search-only path breakage)** ✅ — Returning users guarded from experiment enrollment. + +--- + +## Key Files + +| File | Purpose | +|---|---| +| `iOS/DuckDuckGo/DaxDialogs.swift` | Core onboarding dialog logic, chat-path phase state machine | +| `iOS/DuckDuckGo/OnboardingFlow/LinearOnboarding/OnboardingIntroViewModel.swift` | Experiment enrollment, step injection | +| `iOS/DuckDuckGo/NewTabPageViewController.swift` | Visit-site dialog presentation, toolbar hiding | +| `iOS/DuckDuckGo/MainViewController+DuckAIExperiment.swift` | Chat-path completion, EOJ dialog trigger | +| `iOS/Core/FeatureFlag.swift` | `onboardingDuckAIQueryExperiment` feature flag + cohorts | +| `iOS/DuckDuckGo/UserText.swift` | Onboarding copy strings (DuckAIQueryExperiment, SearchExperience) | +| `iOS/DuckDuckGo/OnboardingFlow/LinearOnboarding/OnboardingView+DuckAIExperimentSearchContent.swift` | DuckAIQuery toggle + experiment UI | +| `iOS/DuckDuckGo/OnboardingFlow/ContextualOnboarding/Rebranding/RebrandedNewTabDaxDialogFactory.swift` | Chat-path dialog creation | + +--- + +## Copy Changes Summary + +| Location | Old | New | Status | +|---|---|---|---| +| `DuckAIQueryExperiment.toggleAILabel` (onboarding toggle) | `Duck.ai` | `Ask.ai` | ✅ Done | +| `searchAndDuckAIOption` (Search Experience screen) | `Toggle between\nSearch and Duck.ai` | *(should remain Duck.ai)* | ❌ Needs revert | + +--- + +## Experiment Config + +```json +{ + "experimentName": "TBD", + "targets": [{ "localeLanguage": "en", "localeCountry": "US" }], + "cohorts": ["control", "treatmentA", "treatmentB"] +} +``` + +> Feature flag: `FeatureFlag.onboardingDuckAIQueryExperiment` +> Cohort type: `FeatureFlag.DuckAIQueryExperimentCohort` (`.control`, `.treatmentA`, `.treatmentB`)