Never Ask Twice
README.md
# Never Ask Twice

**The website already knows the answer. The browser agent can't reach it — until the site hands it over.**
[](LICENSE)




[](https://youtu.be/YggGztPaWpk)
**Live:** **<https://neverasktwice.dev/chat>** — renders in any browser. WebMCP tools register automatically in a WebMCP-capable browser. Add `?webmcp=off` to the same URL for the control condition.
**Demo (2:42):** **<https://youtu.be/YggGztPaWpk>** — *Never Ask Twice — The Website Already Knows.* Real browser, real WebMCP invocation, no scripted footage.
---
## The 15-second version
A support site holds real context about the person visiting it: their service
level, their integrations, their open issue, who they escalate to.
A browser agent standing on that same page cannot reliably use any of it. It
scrapes, it guesses, or it gives up and **asks the human to type it in again** —
the exact thing the person came to the site to avoid.
WebMCP closes that gap. The site declares typed, authorized capabilities; the
agent discovers and calls them.
| | Before this challenge | What the challenge window added |
|---|---|---|
| **Existed** | A support memory engine — working / episodic / semantic tiers, forgetting, budgeted recall — behind a chat UI and a **stdio** MCP server | — |
| **Reachable by a browser agent** | No. Context lived server-side behind a chat box. | **Yes.** Two typed WebMCP tools on `document.modelContext` |
| **Agent can read context** | Falls back to page inspection, then asks the human | Calls `get_support_context`, answers directly |
| **Agent can correct context** | Not possible | `update_escalation_contact`, gated on explicit human confirmation |
| **Tenant safety** | Convention | Mechanism — server-side scope, signed cookie, no model-supplied selectors |
The pre-existing stdio MCP server is **not** WebMCP. It is a separate,
non-browser surface that predates this challenge. Everything scored here is the
browser-native work listed in the right column.
## The two tools
Both are registered on the page via `document.modelContext.registerTool`.
Neither accepts an account, customer, tenant, or session selector — scope is
resolved server-side from a signed visitor cookie, so **the model cannot name
whose data it wants**.
| Tool | Kind | Arguments | Boundary |
|---|---|---|---|
| `get_support_context` | read | `topics[]` from a closed five-value vocabulary, optional | `readOnlyHint`, bounded output, result marked `untrusted` |
| `update_escalation_contact` | mutate | `newContact`, optional `reason` | Requires an explicit human **Confirm and persist** click; atomic supersession; independent readback before success is reported |
## Try it in 60 seconds
Open **<https://neverasktwice.dev/chat>** in a WebMCP-capable browser and watch
the **WebMCP Action Trace** panel while you ask:
**1 — Read.** The prompt used verbatim in the frozen counterfactual:
```
What does support already know about our setup?
```
Trace: `CALLED → SCOPED → RETURNED`. The agent answers with Gold SLA,
Salesforce, Priya — without asking you for any of it.
Run the same prompt at `/chat?webmcp=off` and the agent asks you to paste it in.
**2 — Confirmed correction.** A mutation a human must approve:
```
Priya has left the account. Our escalation contact is now Marcus Chen — please update it.
```
Trace: `CALLED → CONFIRMATION_REQUESTED →` *(you click Confirm and persist)* `→ EXECUTED → OBSERVED`.
Cancel instead, and nothing is written. After committing, ask prompt 1 again —
the old contact is gone and the new one is returned.
## Evidence
Each gate is a frozen document with its raw log beside it. None of it is
reconstructed after the fact.
| Gate | Claim | Document | Raw |
|---|---|---|---|
| G1 | A real browser discovered and executed the tool natively in Chrome | [`docs/webmcp-spike-evidence.md`](docs/webmcp-spike-evidence.md) | [`webmcp-live-verification.jsonl`](docs/webmcp-live-verification.jsonl) |
| G2 | **Counterfactual** — WebMCP ON vs OFF changes the user journey | [`docs/webmcp-counterfactual-comparison.md`](docs/webmcp-counterfactual-comparison.md) | [`webmcp-counterfactual-preconditions.jsonl`](docs/webmcp-counterfactual-preconditions.jsonl) |
| G3 | **Security** — the agent's reach is mechanically tenant-bound | [`docs/webmcp-security-g3.md`](docs/webmcp-security-g3.md) | [`webmcp-security-g3-negative-tests.jsonl`](docs/webmcp-security-g3-negative-tests.jsonl) |
| G4 | **Mutation** — confirmed, atomic, reread before success | [`docs/webmcp-mutation-g4.md`](docs/webmcp-mutation-g4.md) | [`webmcp-mutation-g4-negative-tests.jsonl`](docs/webmcp-mutation-g4-negative-tests.jsonl) |
Each document states its own limits. G2 is one pair, one model, one question,
and it does not claim browser agents *cannot* reach this data without WebMCP.
G4 does not claim prompt-injection immunity.
## Provenance
This is an **Existing Project, meaningfully extended**. The boundary is
mechanical, not asserted:
- Pre-challenge baseline: `c2997996` — tagged `baseline-pre-webmcp`, **zero** WebMCP files.
- All WebMCP work: 2026-09-01 → 2026-09-03.
```bash
git ls-tree -r baseline-pre-webmcp --name-only | grep -c webmcp # 0
git log --oneline baseline-pre-webmcp..HEAD # the challenge window
```
Full table — baseline, challenge additions, deployed code and tree, deployment
id, final commit: **[`docs/webmcp-build-period.md`](docs/webmcp-build-period.md)**.
Third-party assets: [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
## Run it yourself
```bash
pnpm install
pnpm vitest run tests/webmcp-escalation-contact-g4.test.ts \
tests/webmcp-security-g3.test.ts \
tests/webmcp-support-context.test.ts # 57 tests, no secrets needed
```
These are the deterministic security and state-transition tests behind G3 and
G4 — selector rejection, cross-origin rejection, missing confirmation, rollback
under injected failure, retry, and cross-visitor isolation. They need no
database and no API key.
Full local setup (Postgres, migrations, live Qwen path) is in
[Local development](#local-development) below.
---
# Pre-existing system — historical background
> Everything in this section predates the WebMCP Challenge and is **not**
> submitted as challenge work. It is the system the challenge work extends.
Never Ask Twice was built for the **Qwen Cloud Global AI Hackathon — MemoryAgent
track** (July 2026), where it was submitted as a B2B support memory agent.
**Support that remembers.** Customers don't want a smarter chatbot if they still
have to repeat their SLA, setup, open issue, and escalation contact every time
they come back.
The demo agent is **Nat**, powered by **NATE** — the Never Ask Twice Engine — a
scoped memory layer that turns support conversations into durable, auditable
customer context.
## What makes it a MemoryAgent
- **Working memory** — current-session context usable before session-close distillation.
- **Episodic memory** — raw support events with Qwen embeddings and provenance.
- **Semantic memory** — distilled customer facts with confidence, validity windows, and source links.
- **Forgetting policy** — TTL expiry, supersession, stale-memory exclusion, audit-safe provenance.
- **Budgeted recall** — relevant memories only, capped to a strict context budget.
- **stdio MCP surface** — four memory tools for agent interoperability. *(Not WebMCP.)*
This is not transcript logging. It is structured memory with retrieval
discipline, provenance, forgetting, and measurable cross-session improvement.
## The deterministic eval harness
```bash
pnpm eval
```
Expected fixture output:
```text
memory-on re-ask rate: 0.00
memory-on recall accuracy: 1.00
memory-off re-ask rate: 1.00
memory-off recall accuracy: 0.00
re-ask rate: 0.00 (memory) vs 1.00 (no-memory)
```
**Read this as a fixture assertion, not a measurement.** The harness is
intentionally deterministic for reproducible scoring: fixed synthetic fixtures
and a fake Qwen client. It is a regression guard on the memory policy, not a
live benchmark, and it is unrelated to the WebMCP claims above. Details:
[`docs/evaluation.md`](docs/evaluation.md).
## Prior-period status
|Area|Status|Notes|
|---|---|---|
|Public clean-room repo|Done|Synthetic data only; boundary scan included.|
|Memory service|Done|Working, episodic, semantic, forgetting, budgeted recall.|
|stdio MCP surface|Done|Four memory tools via `pnpm mcp:list-tools`.|
|Qwen-backed live path|Done|`/health` reports `mode: "qwen-live"`.|
|Railway deployment|Live — judge-clickable|[`neverasktwice.dev`](https://neverasktwice.dev/chat) renders in a browser; re-verified 2026-09-02. See [`deploy/railway.md`](deploy/railway.md).|
|Alibaba FC deployment|Live — verify by curl|`curl https://never-awice-api-kvsvpczulb.us-east-1.fcapp.run/health`. Alibaba forces `Content-Disposition: attachment` on the free `*.fcapp.run` subdomain, so a browser downloads instead of rendering — platform policy, not a broken deploy. See [`deploy/alibaba-fc.md`](deploy/alibaba-fc.md).|
|Qwen-hackathon demo video|Done — **superseded**|[Watch](https://youtu.be/P254DPj-Mgw) — frozen Acme scenario, July 2026. **This is not the WebMCP Challenge video.** The challenge demo is [above](#never-ask-twice).|
|Build log|Done|[Building customer support memory that survives an audit](https://marcellelabs.io/insights/building-customer-support-memory-survives-audit)|
## Architecture
```text
Browser agent (WebMCP) Customer chat / stdio MCP
| |
v v
document.modelContext Hono API
get_support_context --> /webmcp/support-context
update_escalation_contact --> /webmcp/escalation-contact/{propose,commit}
| |
+--------> scope resolved server-side from signed visitor cookie
|
v
MemoryService
|-- working memory
|-- episodic memory (+ Qwen embeddings)
|-- semantic memory
|-- forgetting: TTL + supersession
|
+--> Qwen Cloud (DashScope-compatible API)
+--> Postgres + pgvector
```
Full diagram and component map: [`docs/architecture.md`](docs/architecture.md).
## Brand assets
Logo files and usage rules: [`docs/assets/brand`](docs/assets/brand).
Tagline: **Support that remembers.** Descriptor: **Enterprise Support MemoryAgent.**
---
# Local development
### 1. Clone and install
```bash
git clone https://github.com/marcelle-labs/never-ask-twice.git
cd never-ask-twice
pnpm install
```
### 2. Configure environment
```bash
cp .env.example .env
```
Set `DASHSCOPE_API_KEY` for live Qwen-backed embeddings, distillation, and
adjudication. The example is pre-filled for local Postgres on port 5433.
```env
DATABASE_URL=postgresql://neverasktwice:neverasktwice@localhost:5433/neverasktwice
DASHSCOPE_API_KEY=your-key-here
QWEN_BASE_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1
QWEN_CHAT_MODEL=qwen-plus
QWEN_EMBEDDING_MODEL=text-embedding-v3
QWEN_EMBEDDING_DIM=1024
MEMORY_TOKEN_BUDGET=1200
```
Without `DASHSCOPE_API_KEY`, the API boots in local-safe mode: zero-vector
embeddings and empty distillation responses so the server runs without secrets.
It does not perform real Qwen work.
### 3–7. Database, eval, and server
```bash
docker compose up -d # Postgres on localhost:5433
pnpm migrate
pnpm eval # deterministic fixture harness
pnpm boundary-scan # clean-room scan
pnpm dev # API on http://localhost:3000
```
API endpoints:
- `GET /health` — health and capability status.
- `GET /webmcp/support-context` — WebMCP read surface (same-origin, cookie-scoped).
- `POST /webmcp/escalation-contact/propose` · `/commit` — WebMCP confirmed mutation.
- `POST /turn` — append a customer/agent turn.
- `POST /sessions/:id/close` — close a session and distill episodic → semantic.
- `POST /recall` — recall a bounded memory bundle.
### 8. Run the stdio MCP server
```bash
pnpm build
node dist/src/mcp/server.js
```
Exposes `recall_memory`, `write_memory`, `distill_session`, and `forget`.
## Project structure
- `apps/api` — Hono API, local server, FC handler, and the `/chat` UI.
- `apps/api/src/webmcp` — **WebMCP surface**: support-context read, escalation-contact mutation, scope resolution.
- `src/agent` — deterministic support-agent policy used by the eval harness.
- `src/contracts.ts` — memory predicate enum, Zod contracts, shared types.
- `src/db` — Drizzle schema and SQL migration string.
- `src/memory` — memory service, stores, retrieval, supersession, forgetting.
- `src/mcp` — stdio MCP surface over the shared memory service.
- `src/qwen` — single Qwen Cloud client module.
- `src/testing` — deterministic fake Qwen client.
- `eval` — frozen three-session scenario, ground truth, expected output, runner.
- `scripts` — boundary scan, migration, MCP list-tools, WebMCP security evidence.
- `docs` — architecture, memory model, evaluation, forgetting, and all WebMCP evidence.
- `deploy` — Railway and Alibaba FC deployment proofs.
## Key commands
|Command|Purpose|
|---|---|
|`pnpm install`|Install dependencies|
|`pnpm build`|Build the project|
|`pnpm lint`|TypeScript type check|
|`pnpm test`|Full test suite|
|`pnpm eval`|Deterministic memory ON/OFF fixture harness|
|`pnpm migrate`|Run database migrations|
|`pnpm boundary-scan`|Clean-room boundary scan|
|`pnpm mcp:list-tools`|List the stdio MCP tools|
|`pnpm demo:script-check`|Verify demo fixtures are aligned|
## Security and clean-room boundary
Never Ask Twice uses synthetic data only. Do not commit real customer data,
secrets, `.env` files, or private platform identifiers. The repository includes
a boundary scan that fails on known forbidden tokens, and a local-safe mode so
judges can run the server without secrets.
See [`SECURITY.md`](SECURITY.md) and [`docs/webmcp-security-g3.md`](docs/webmcp-security-g3.md).
## License
Apache-2.0 — see [`LICENSE`](LICENSE). Third-party notices: [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessUnresponsive