Development Journey — Email Triage with Jev over 90 days of Gmail
you@gmail, you@yahoo, you@hotmail), one bank name (now "Bank"), one agency name (now "[redacted]"), one Google Cloud billing account number (now "[redacted]"), four Google Cloud payment amounts, one budget amount, and two subscription amounts (all now "$[redacted]"), and the user's first name (now "[name]" on this page; `triage.mjs` in the repo now says "the user", changed after the run, so the numbers on this page came from the question text with the name in it). A build script did the replacement and refused to write the page if any original string survived. The files on disk under .ignore/ are unredacted, untracked, and listed in .gitignore. The Jev numbers are unchanged. Jev saw the unredacted text.
1. The brief — triage a real inbox, four questions per email
This session started with /clear and one line: @HANDOFF.md. The handoff file from the previous session named the next task: run Jev over the user's Gmail with four questions per email, and ask the user for a label and a date range first. The user answered "1 INBOX 2 last 90 days". That was the whole brief for the build. The brief for this document came later: write a journey like the router one, explain the plumbing, show every structured file, surface the unknown unknowns and address them, redact three things, include all the code, and explain the gateway's role.
The four questions come from Riley Brown's video, where he runs Jev over 500 of his own emails. His four: a Noul "mentions a brand deal opportunity", a Choice "category of email" with five options (brand deal, subscription update, cold pitch, newsletter, agent native), a Score "how important is it that Riley personally sees this" on a six-level scale (ignore, low, medium, high, critical, insane), and a second Noul "is this a scam email". This session kept the four question types and the six-level scale, and rewrote the category list for a different inbox. Section 5 has the exact text.
The same invisible constraints as last time shaped the code and the prose: ASD-STE100 Simplified Technical English for every reply, the "ponytail" minimal-code mode at level full, the "precise phrasing, never rhetorical" rule, and the user's standing rules (questions are read-only; add no file that was not asked for; finish, or say what you left). One extra rule from the harness applied: commit only when the user asks. So triage.mjs sat uncommitted until the user chose "commit and push" for this document.
2. Cold start — what the handoff carried, and what it did not
Minute zero: a git repo on main at 85c4360, clean, pushed. ai 7.0.107 installed. Node v22.22.0. The gateway key in .env as AI_GATEWAY_API_KEY, on the paid tier since the previous evening. HANDOFF.md untracked and read first. One memory file from the previous session, jev-gateway-facts.md, loaded by the harness.
Reused, not re-derived
- Model id
typesafe-ai/jev. Call shapeexperimental_evaluate({ model, state, questions }). Run withnode --env-file=.env. - Confidence is not in the answer. It is at
providerMetadata.typesafe.confidence.<questionId>.server.mjsalready read it there;triage.mjscopied that line. - The three question types on the gateway are
choice,score,boolean.probe.mjsfrom the previous session had sent abooleanand it worked. - The 20,000-character cap idea from
server.mjs. Not needed here: the longest snippet is 230 characters.
Not carried, and found this session
- The
scorequestion shape. Fetched fromhttps://docs.typesafe.ai/primitives/score.md:criteriais an ordered array, 2 to 10 levels, low to high. The answer hasscore(fractional, 0 to max level),probabilitieskeyed by level number as a string, and on the direct API alegend. The gateway response had nolegend. - The answer field name of a
boolean. No doc page read in either session states it for the gateway. It was learned from a raw run:probability. Section 9 explains why that mattered. - The confidence map has keys only for the choice and score questions. The two boolean questions get no confidence entry. Their
probabilityis the whole answer.
Rule learned: when a field name is not in any doc you read, run one call with the raw body printed before you write the code that reads the field.
3. How the 90 days were fetched — one MCP call, then a hand-typed file
This is the part a reader would most expect to be a script. It is not. The Gmail connection is a claude.ai-hosted MCP server. Its tools are callable from inside the Claude Code session only. A Node script on the user's machine has no Gmail credentials and cannot call it. So the fetch happened in the agent's turn, and the result was carried into a file by the agent.
The call
mcp__claude_ai_Gmail__search_threads({
query: "in:inbox newer_than:90d",
pageSize: 50,
view: "THREAD_VIEW_MINIMAL"
})
What came back: resultCountEstimate: "31", 31 threads, 43 messages inside them, and no nextPageToken. So one page held everything. The tool's pageSize maximum is 50; a larger inbox needs a loop on nextPageToken. Each message in THREAD_VIEW_MINIMAL carries id, threadId, date, sender, toRecipients, subject, snippet, labelIds, sizeEstimate. No body. A body needs one get_message call per message with messageFormat: "PLAIN_TEXT". None was made.
The transcription
The agent then wrote .ignore/emails.json by hand from that tool result: one record per message with five fields, id, date, from, subject, snippet. While typing it did four things that no script recorded:
- Dropped 2 of the 43 messages. Both had
labelIds: ["SENT"]only: the user's replies "Re: call [redacted]" and "Re: Library book due". They live in inbox threads but are not inbox mail. One self-sent message with["SENT","INBOX"]was kept. 41 records remain. - Decoded HTML entities in snippets:
'to an apostrophe,&to an ampersand. - Trimmed the date to
YYYY-MM-DDfrom the ISO timestamp. - Cut three fragments out of snippets before Jev saw them: a card's last four digits, a
Payment ID: CLOUD …token, and aOrder number: SOP.…token. So the Google Play order number never reached the file and needs no redaction on this page.
What was in the 90 days
Counting by the category Jev later assigned: 18 account or security notices (15 of them the same OAuth-consent "Security alert" repeated inside five threads), 8 receipts and billing notices, 7 service notices, 2 newsletters, 4 reminders the user or a family member sent to the user, 1 test "contract" mail the user sent to themself, 1 self-sent summary. No mail from a stranger. No brand deal. The inbox is a machine-mail inbox with a few human notes. That shape drives every finding in section 7.
The 30-day question, answered
After the run the user asked how to run the triage on the last 30 days. The answer is the same two-step: the agent runs search_threads with newer_than:30d and writes the JSON; the script reads the JSON. A standalone fetcher would need a Google Cloud OAuth client, the googleapis package, and a browser login. That is about 60 lines plus console setup. It was not built. Nobody asked for it.
4. How the Vercel AI Gateway sits in the path
Section 3 of the router journey has the wire protocol, read from @ai-sdk/gateway source. This section restates only what the triage depends on, and adds what this session's raw output showed.
The path for one email is four hops. The script calls experimental_evaluate from the ai package with a plain string model id. Because AI_GATEWAY_API_KEY is set and the id has a slash, the SDK resolves it through the gateway provider. The SDK sends POST https://ai-gateway.vercel.sh/v4/ai/evaluation-model with the header ai-model-id: typesafe-ai/jev and a JSON body of state and questions. The gateway forwards the body to TypeSafe with Vercel's own credential (the routing block says credentialType: "system"). TypeSafe answers. The gateway wraps the answer, moves the confidence values into providerMetadata.typesafe, adds its own gateway block with routing, cost, and a generation id, and returns it. The SDK parses that into ev.answers and ev.providerMetadata.
What the gateway gives the triage that a direct TypeSafe call would not: one key for Jev and for any reply model, billing on the same credit balance, and the routing block below. What it takes away: the legend on score answers, and the confidence inside each answer, which is moved. What it does not change: the four questions travel in one request. The gateway makes one call to TypeSafe per email, not four.
The gateway block from this session's first call
"gateway": {
"routing": {
"originalModelId": "typesafe-ai/jev",
"resolvedProvider": "typesafe-ai",
"fallbacksAvailable": [],
"planningReasoning": "System credentials planned for: typesafe-ai. Total execution order: typesafe-ai(system)",
"canonicalSlug": "typesafe-ai/jev",
"finalProvider": "typesafe-ai",
"modelAttemptCount": 1,
"modelAttempts": [ { "canonicalSlug": "typesafe-ai/jev", "success": true, "providerAttemptCount": 1,
"providerAttempts": [ { "provider": "typesafe-ai", "credentialType": "system", "success": true,
"startTime": 1789798523337, "endTime": 1789798523503, "statusCode": 200 } ] } ],
"totalProviderAttemptCount": 1
},
"cost": "0",
"marketCost": "0.000029778",
"surchargeCost": "0",
"gatewayCost": "0",
"generationId": "gen_01M2W4W89FAPM26YJNT30M8V3N"
}
Provider-side time: endTime minus startTime is 166 ms for four questions over one email. The router's one-question probe last session took 138 ms.
cost is "0" while marketCost is "0.000029778". Last session the same pair was explained by free credits. This account has been on the paid tier since 2026-09-18. That explanation no longer holds. No page read in either session defines the difference between cost and marketCost on a paid account. The credits endpoint was not checked this session. Treat marketCost as the price and cost as unexplained until you read the pricing page or the credits balance.
5. The four questions, and what Jev sees
The block a reviewer must read. It is the top of triage.mjs.
const QUESTIONS = {
category: {
type: "choice",
instructions: "What kind of email is this, received by [name]?",
criteria: {
security_alert: "An account security or sign-in notice from a service [name] uses",
receipt: "A payment receipt, invoice, or billing notice",
service_notice: "A product, policy, or terms change from a service [name] uses",
newsletter: "A digest, marketing, or promotional mailing",
personal_reminder: "A reminder or task sent by [name] or a family member to [name]",
business_inquiry: "A person or company asking [name] for work, a contract, or a deal",
other: "None of the above",
},
},
importance: {
type: "score",
instructions: "How important is it that [name] personally reads this email?",
criteria: [
"ignore: no action and no information [name] needs",
"low: informational only; fine to skim later",
"medium: worth reading this week",
"high: needs an action or a reply within a few days",
"critical: needs an action today; money, security, or a deadline is at stake",
"insane: needs a response within 30 minutes or bad things happen",
],
},
brand_deal: { type: "boolean", instructions: "Does this email mention a sponsorship or brand deal opportunity for [name]?" },
scam: { type: "boolean", instructions: "Does this email look like a scam, phishing, or something untrustworthy?" },
};
The state for one email is an object with four keys:
state: { from: e.from, subject: e.subject, date: e.date, snippet: e.snippet }
What reaches Jev
- The state object, serialized. The key names
from,subject,date,snippetare input. Renamesnippettobodyand the answers can shift. - For the choice: the instruction, the seven option names, and the seven rubric strings. The option name is input.
security_alertas a word helps Jev match an email whose subject is "Security alert". - For the score: the instruction and the six level strings, in order. The level label is part of the string, so Jev reads "critical: needs an action today; …" as one sentence. The number 4 is what comes back.
- For each boolean: the instruction only. No criteria map was given. The direct API's Noul takes a
{ true: …, false: … }map; the gateway's boolean accepted the question without one.
What never reaches Jev
- The question ids
category,importance,brand_deal,scam. The docs say the model never sees the id. - Today's date. Jev received
date: "2026-07-15"on a row and had no "today" to subtract from it. Section 7 shows what that did. - Who the user is. The instruction names "[name]". The state names senders. Nothing tells Jev that
you@yahooandyou@hotmailare the user's own addresses. - The email body. Only Gmail's snippet, about 100 to 230 characters.
- The other 40 emails. Each call is independent. Jev cannot see that the same "Security alert" arrived four times in one thread.
Why these seven categories and not Riley's five
Riley's list (brand deal, subscription update, cold pitch, newsletter, agent native) fits a creator's inbox. The tool result showed what this inbox holds. The advisor's first consultation said: use Riley's list if it fits, else pick from what is visible, and add other because the router journey flagged the missing-none problem. The seven above were picked from the 43 messages on screen. other caught exactly one row: the self-sent chat summary.
6. The run — input file, output file, cost
The script, in full
triage.mjs as committed. Run: node --env-file=.env triage.mjs .ignore/emails.json [--limit N] [--raw]. --limit takes the first N records. --raw prints the full answer and metadata for each call before the table.
// Email triage with Jev. Run: node --env-file=.env triage.mjs .ignore/emails.json [--limit N] [--raw]
// Input: JSON array of { id, date, from, subject, snippet }. Output: <input>.triage.json + a table on stdout.
import { readFile, writeFile } from "node:fs/promises";
import { experimental_evaluate as evaluate } from "ai";
// ---- Review these. Everything else is plumbing. ----
const JEV = "typesafe-ai/jev";
const QUESTIONS = {
category: {
type: "choice",
instructions: "What kind of email is this, received by [name]?",
criteria: {
security_alert: "An account security or sign-in notice from a service [name] uses",
receipt: "A payment receipt, invoice, or billing notice",
service_notice: "A product, policy, or terms change from a service [name] uses",
newsletter: "A digest, marketing, or promotional mailing",
personal_reminder: "A reminder or task sent by [name] or a family member to [name]",
business_inquiry: "A person or company asking [name] for work, a contract, or a deal",
other: "None of the above",
},
},
importance: {
type: "score",
instructions: "How important is it that [name] personally reads this email?",
criteria: [
"ignore: no action and no information [name] needs",
"low: informational only; fine to skim later",
"medium: worth reading this week",
"high: needs an action or a reply within a few days",
"critical: needs an action today; money, security, or a deadline is at stake",
"insane: needs a response within 30 minutes or bad things happen",
],
},
brand_deal: { type: "boolean", instructions: "Does this email mention a sponsorship or brand deal opportunity for [name]?" },
scam: { type: "boolean", instructions: "Does this email look like a scam, phishing, or something untrustworthy?" },
};
const args = process.argv.slice(2);
const file = args.find((a) => !a.startsWith("--"));
const limit = Number(args[args.indexOf("--limit") + 1]) || Infinity;
const raw = args.includes("--raw");
const emails = JSON.parse(await readFile(file, "utf8")).slice(0, limit);
const out = [];
for (const e of emails) {
const ev = await evaluate({ model: JEV, state: { from: e.from, subject: e.subject, date: e.date, snippet: e.snippet }, questions: QUESTIONS });
if (raw) console.log(JSON.stringify({ answers: ev.answers, meta: ev.providerMetadata }, null, 2));
const a = ev.answers, conf = ev.providerMetadata?.typesafe?.confidence ?? {};
out.push({
id: e.id, date: e.date, from: e.from, subject: e.subject,
category: a.category.choice, categoryConf: conf.category ?? null,
importance: a.importance.score, importanceConf: conf.importance ?? null,
brandDeal: a.brand_deal.probability,
scam: a.scam.probability,
});
}
await writeFile(file.replace(/\.json$/, ".triage.json"), JSON.stringify(out, null, 2));
const f = (n) => (n == null ? " - " : n.toFixed(2));
console.log("imp conf scam deal category date from subject");
for (const r of [...out].sort((x, y) => y.importance - x.importance))
console.log(`${f(r.importance)} ${f(r.importanceConf)} ${f(r.scam)} ${f(r.brandDeal)} ${r.category.padEnd(18)} ${r.date} ${r.from.slice(0, 28).padEnd(28)} ${r.subject.slice(0, 60)}`);
The first run: one email, raw
node --env-file=.env triage.mjs .ignore/emails.json --limit 1 --raw. The gateway block is in section 4. The answers block, verbatim:
{
"answers": {
"category": {
"type": "choice",
"choice": "security_alert",
"probabilities": { "receipt": 0, "other": 0, "business_inquiry": 0, "personal_reminder": 0,
"service_notice": 0, "security_alert": 1, "newsletter": 0 }
},
"importance": {
"type": "score",
"score": 1.49,
"probabilities": { "0": 0.02, "1": 0.68, "2": 0.15, "3": 0.08, "4": 0.07, "5": 0 }
},
"brand_deal": { "type": "boolean", "probability": 0.01 },
"scam": { "type": "boolean", "probability": 0.17 }
},
"meta": {
"typesafe": { "confidence": { "category": 0.99, "importance": 0.64 } },
"gateway": { ... }
}
}
Three readings. The score 1.49 is the probability-weighted mean of the levels: 0×0.02 + 1×0.68 + 2×0.15 + 3×0.08 + 4×0.07 + 5×0 = 1.50 from the printed probabilities; the API computed 1.49 from the unrounded ones. The boolean answer has one field, probability, and no confidence entry. The category confidence is 0.99 while its top probability prints as 1, which is the two-decimal rounding from last session again.
The input file
Redacted copy of .ignore/emails.json. 41 records. The file on disk is unredacted and untracked.
Show emails.json (41 records)
[
{"id":"1a0b6db99d5bc7d2","date":"2026-09-18","from":"noreply-accounts@google.com","subject":"You shared some Google Account data with typesafe.ai","snippet":"Keep track of your Google Account data. You're receiving this email because you used Sign in with Google to sign in to typesafe.ai on September 18 at 4:30 PM."},
{"id":"1a088d2075a2e8d6","date":"2026-09-10","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Muse access to some of your Google Account data. If you didn't allow Muse access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"1a088d27e2cd4df5","date":"2026-09-10","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Muse access to some of your Google Account data. If you didn't allow Muse access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"1a088d2aca4ee4f7","date":"2026-09-10","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Muse access to some of your Google Account data. If you didn't allow Muse access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"1a088d342f88268e","date":"2026-09-10","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Muse access to some of your Google Account data. If you didn't allow Muse access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"1a086b561e6a5a1e","date":"2026-09-07","from":"no-reply@accounts.google.com","subject":"Critical security alert","snippet":"Some of your saved passwords were found online. Some of your saved passwords were found in a data breach from a site or app that you use. Your Google Account is not affected."},
{"id":"1a05e5150f301756","date":"2026-09-01","from":"payments-noreply@google.com","subject":"Google: We've received your payment for [redacted]","snippet":"Google Cloud Platform & APIs. Payment received. Your payment amount of $[redacted] to Google was received on Sep 1, 2026. Payment for: Google Cloud. Payment method: Visa."},
{"id":"1a059e20e30f192c","date":"2026-08-31","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Activepieces access to some of your Google Account data. If you didn't allow Activepieces access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"1a059e2612c87f01","date":"2026-08-31","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Activepieces access to some of your Google Account data. If you didn't allow Activepieces access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"1a059e2ac0da8483","date":"2026-08-31","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Activepieces access to some of your Google Account data. If you didn't allow Activepieces access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"1a03aeb2c5cd918e","date":"2026-08-25","from":"googledevelopers-noreply@google.com","subject":"[Action Advised] Manage your unused OAuth clients and protect your information","snippet":"Your projects have inactive OAuth clients that are subject to automatic deletion. Hello Google Developer, In accordance with OAuth 2.0 Policies, the following projects that you manage have OAuth clients that have been inactive."},
{"id":"1a03661ef374ec59","date":"2026-08-25","from":"googleplay-noreply@google.com","subject":"Your Google Play Order Receipt from Aug 24, 2026","snippet":"Google Play. Thank you. Your subscription from Google LLC on Google Play continues and you've been charged. Manage your subscriptions. Order date: Aug 24."},
{"id":"1a02142922347f47","date":"2026-08-20","from":"contact@alphaxiv.org","subject":"Introducing Researchers To Follow","snippet":"alphaXiv Weekly digest. Hi [name], We're rolling out personalized recommendations across alphaXiv. Your weekly emails will now feature papers and researchers tailored to your interests."},
{"id":"1a01594b5dc46a58","date":"2026-08-18","from":"noreply-accounts@google.com","subject":"You shared some Google Account data with DocuSign Demo","snippet":"Keep track of your Google Account data. You're receiving this email because you used Sign in with Google to sign in to DocuSign Demo on August 18 at 8:54 AM."},
{"id":"1a012a74ec1759c1","date":"2026-08-18","from":"alerts@notify.bank.example","subject":"Bank Change to Your Delivery Preference","snippet":"Bank. Your delivery preferences have changed. Your delivery preference(s) for how you will receive your documents were updated for one or more of your brokerage accounts."},
{"id":"19ffc4a0277a14a6","date":"2026-08-13","from":"payments-noreply@google.com","subject":"Google: We've received your payment for [redacted]","snippet":"Google Cloud Platform & APIs. Payment received. Your payment amount of $[redacted] to Google was received on Aug 13, 2026. Payment for: Google Cloud. Payment method: Visa."},
{"id":"19ff8254ca7ac6a1","date":"2026-08-12","from":"googleaistudio-noreply@google.com","subject":"[Action Required] Update your Gemini API billing in Google AI Studio by Oct 12, 2026","snippet":"Google AI Studio is transitioning from Postpay to Prepay billing for Gemini API usage. Hello [name], We are writing to inform you of an upcoming billing change to Google AI Studio and the Gemini API."},
{"id":"19ff7d076c364e5a","date":"2026-08-12","from":"googleaistudio-noreply@google.com","subject":"Upgrade your Gemini 2.5 Flash workloads","snippet":"Get higher intelligence and better throughput with Gemini 3.5 Flash-Lite."},
{"id":"19ff2360c1938b3e","date":"2026-08-11","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Cursor access to some of your Google Account data. If you didn't allow Cursor access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"19ff23663fb3e5ab","date":"2026-08-11","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Cursor access to some of your Google Account data. If you didn't allow Cursor access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"19ff2369dd071170","date":"2026-08-11","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Cursor access to some of your Google Account data. If you didn't allow Cursor access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"19ff170e3538fc02","date":"2026-08-11","from":"portraits-noreply@google.com","subject":"Portraits is shutting down on September 14, 2026","snippet":"Hi, We're writing to let you know that we will be turning down Portraits on September 14, 2026. Our goal in Labs is to test new ideas and get your feedback on experimental AI; thank you so much for participating."},
{"id":"19fe0568e97bad68","date":"2026-08-08","from":"CloudPlatform-noreply@google.com","subject":"50% of budget reached","snippet":"Google Cloud. Billing Budget Alert. 50% of budget reached. Billing account: My Billing Account. Aug 8, 2026. Budget Amount $[redacted]. Budget Period Aug 1, 2026 - Aug 31, 2026."},
{"id":"19fd77b5b731bc69","date":"2026-08-06","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Zapier access to some of your Google Account data. If you didn't allow Zapier access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"19fd78662684bf00","date":"2026-08-06","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Zapier access to some of your Google Account data. If you didn't allow Zapier access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"19fbe7509db41cd0","date":"2026-08-01","from":"payments-noreply@google.com","subject":"Google: We've received your payment for [redacted]","snippet":"Google Cloud Platform & APIs. Payment received. Your payment amount of $[redacted] to Google was received on Aug 1, 2026. Payment for: Google Cloud. Payment method: Visa."},
{"id":"19f9e0e42d8a044e","date":"2026-07-26","from":"you@gmail","subject":"Summary of chat sessions from the last five hours","snippet":"Summary of chat sessions from the last five hours: There was one chat session. We opened the Claude Voice meeting draft, corrected the recipient to you@yahoo, and sent the email."},
{"id":"19f96bcce5c083d1","date":"2026-07-25","from":"googleplay-noreply@google.com","subject":"Your Google Play Order Receipt from Jul 24, 2026","snippet":"Google Play. Thank you. Your subscription from Google LLC on Google Play continues and you've been charged. Manage your subscriptions. Order date: Jul 24."},
{"id":"19f6dc4d38966096","date":"2026-07-17","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Grok Business access to some of your Google Account data. If you didn't allow Grok Business access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"19f6dc65035dcfde","date":"2026-07-17","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Grok Business access to some of your Google Account data. If you didn't allow Grok Business access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"19f6dc6a0bf0b09c","date":"2026-07-17","from":"no-reply@accounts.google.com","subject":"Security alert","snippet":"You allowed Grok Business access to some of your Google Account data. If you didn't allow Grok Business access to some of your Google Account data, someone else may be trying to access your account."},
{"id":"19f68b0f10462e50","date":"2026-07-16","from":"you@yahoo","subject":"contract","snippet":"we are interested in signing a 10-year contract with you."},
{"id":"19f675333f10c928","date":"2026-07-15","from":"googledevelopers-noreply@google.com","subject":"[Action Advised] Manage your unused OAuth clients and protect your information","snippet":"Your projects have inactive OAuth clients that are subject to automatic deletion. Hello Google Developer, In accordance with OAuth 2.0 Policies, the following projects that you manage have OAuth clients that have been inactive."},
{"id":"19f66180662d15f9","date":"2026-07-15","from":"you@yahoo","subject":"call [redacted]","snippet":"please reply if you receive this reminder: you are many months behind contacting [redacted]. act on it"},
{"id":"19f661703593e7db","date":"2026-07-15","from":"you@yahoo","subject":"check tire","snippet":"Please reply if you receive this reminder: You need to check tire pressure by this Sat. 7/18/2026"},
{"id":"19f661609cded48f","date":"2026-07-15","from":"you@hotmail","subject":"AI subscriptions","snippet":"Here is a reminder to cancel ElevenLabs subscription in 7 days. You should revert Anthropic subscription to $[redacted] from $[redacted]. You may want to bump up OpenAI to $[redacted]"},
{"id":"19f66141392887bc","date":"2026-07-15","from":"you@hotmail","subject":"Library book due","snippet":"I want to remind you that \"Big Book of Sherlock Holmes\" will be due in 24 hours. Please renew or return the book"},
{"id":"19f1f0332f4712c6","date":"2026-07-01","from":"payments-noreply@google.com","subject":"Google: We've received your payment for [redacted]","snippet":"Google Cloud Platform & APIs. Payment received. Your payment amount of $[redacted] to Google was received on Jul 1, 2026. Payment for: Google Cloud. Payment method: Visa."},
{"id":"19f0565c95cd1660","date":"2026-06-26","from":"google-noreply@google.com","subject":"Learn more about our updated Terms of Service","snippet":"Every couple of years, we update our Terms of Service. We wanted to let you know ahead of time that the next update will be on July 30, 2026. These changes won't affect the way you use our services."},
{"id":"19efc3e0bdf315ac","date":"2026-06-25","from":"workspace-noreply@google.com","subject":"Your Google AI subscription and Google Workspace Experiments terms","snippet":"Thank you for being a trusted tester."},
{"id":"19efc3e002bc2442","date":"2026-06-25","from":"googleplay-noreply@google.com","subject":"Your Google Play Order Receipt from Jun 24, 2026","snippet":"Google Play. Thank you. Your subscription from Google LLC on Google Play continues and you've been charged. Manage your subscriptions. Order date: Jun 24."}
]
The output file
Redacted copy of .ignore/emails.triage.json. One record per input, same order, ten fields. importance is the weighted mean on the 0 to 5 scale. categoryConf and importanceConf are the gateway's confidence values. brandDeal and scam are the boolean probabilities.
Show emails.triage.json (41 records)
[
{
"id": "1a0b6db99d5bc7d2",
"date": "2026-09-18",
"from": "noreply-accounts@google.com",
"subject": "You shared some Google Account data with typesafe.ai",
"category": "security_alert",
"categoryConf": 1,
"importance": 1.51,
"importanceConf": 0.63,
"brandDeal": 0.01,
"scam": 0.19
},
{
"id": "1a088d2075a2e8d6",
"date": "2026-09-10",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.92,
"importanceConf": 0.93,
"brandDeal": 0.01,
"scam": 0.41
},
{
"id": "1a088d27e2cd4df5",
"date": "2026-09-10",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.92,
"importanceConf": 0.93,
"brandDeal": 0.01,
"scam": 0.42
},
{
"id": "1a088d2aca4ee4f7",
"date": "2026-09-10",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.93,
"importanceConf": 0.94,
"brandDeal": 0.01,
"scam": 0.41
},
{
"id": "1a088d342f88268e",
"date": "2026-09-10",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.92,
"importanceConf": 0.93,
"brandDeal": 0.01,
"scam": 0.44
},
{
"id": "1a086b561e6a5a1e",
"date": "2026-09-07",
"from": "no-reply@accounts.google.com",
"subject": "Critical security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 2.9,
"importanceConf": 0.52,
"brandDeal": 0.01,
"scam": 0.38
},
{
"id": "1a05e5150f301756",
"date": "2026-09-01",
"from": "payments-noreply@google.com",
"subject": "Google: We've received your payment for [redacted]",
"category": "receipt",
"categoryConf": 1,
"importance": 0.89,
"importanceConf": 0.9,
"brandDeal": 0.01,
"scam": 0.16
},
{
"id": "1a059e20e30f192c",
"date": "2026-08-31",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.88,
"importanceConf": 0.9,
"brandDeal": 0.01,
"scam": 0.3
},
{
"id": "1a059e2612c87f01",
"date": "2026-08-31",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.88,
"importanceConf": 0.9,
"brandDeal": 0.01,
"scam": 0.3
},
{
"id": "1a059e2ac0da8483",
"date": "2026-08-31",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.87,
"importanceConf": 0.9,
"brandDeal": 0.01,
"scam": 0.32
},
{
"id": "1a03aeb2c5cd918e",
"date": "2026-08-25",
"from": "googledevelopers-noreply@google.com",
"subject": "[Action Advised] Manage your unused OAuth clients and protect your information",
"category": "service_notice",
"categoryConf": 0.59,
"importance": 2.81,
"importanceConf": 0.77,
"brandDeal": 0.01,
"scam": 0.45
},
{
"id": "1a03661ef374ec59",
"date": "2026-08-25",
"from": "googleplay-noreply@google.com",
"subject": "Your Google Play Order Receipt from Aug 24, 2026",
"category": "receipt",
"categoryConf": 1,
"importance": 1.7,
"importanceConf": 0.5,
"brandDeal": 0.01,
"scam": 0.2
},
{
"id": "1a02142922347f47",
"date": "2026-08-20",
"from": "contact@alphaxiv.org",
"subject": "Introducing Researchers To Follow",
"category": "newsletter",
"categoryConf": 0.91,
"importance": 1.07,
"importanceConf": 0.91,
"brandDeal": 0.02,
"scam": 0.26
},
{
"id": "1a01594b5dc46a58",
"date": "2026-08-18",
"from": "noreply-accounts@google.com",
"subject": "You shared some Google Account data with DocuSign Demo",
"category": "security_alert",
"categoryConf": 1,
"importance": 1.34,
"importanceConf": 0.73,
"brandDeal": 0.01,
"scam": 0.19
},
{
"id": "1a012a74ec1759c1",
"date": "2026-08-18",
"from": "alerts@notify.bank.example",
"subject": "Bank Change to Your Delivery Preference",
"category": "service_notice",
"categoryConf": 0.52,
"importance": 2.74,
"importanceConf": 0.55,
"brandDeal": 0.01,
"scam": 0.36
},
{
"id": "19ffc4a0277a14a6",
"date": "2026-08-13",
"from": "payments-noreply@google.com",
"subject": "Google: We've received your payment for [redacted]",
"category": "receipt",
"categoryConf": 1,
"importance": 0.91,
"importanceConf": 0.89,
"brandDeal": 0.01,
"scam": 0.17
},
{
"id": "19ff8254ca7ac6a1",
"date": "2026-08-12",
"from": "googleaistudio-noreply@google.com",
"subject": "[Action Required] Update your Gemini API billing in Google AI Studio by Oct 12, 2026",
"category": "service_notice",
"categoryConf": 0.98,
"importance": 2.98,
"importanceConf": 0.67,
"brandDeal": 0.01,
"scam": 0.4
},
{
"id": "19ff7d076c364e5a",
"date": "2026-08-12",
"from": "googleaistudio-noreply@google.com",
"subject": "Upgrade your Gemini 2.5 Flash workloads",
"category": "newsletter",
"categoryConf": 0.65,
"importance": 1.42,
"importanceConf": 0.61,
"brandDeal": 0.01,
"scam": 0.42
},
{
"id": "19ff2360c1938b3e",
"date": "2026-08-11",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.95,
"importanceConf": 0.95,
"brandDeal": 0.01,
"scam": 0.4
},
{
"id": "19ff23663fb3e5ab",
"date": "2026-08-11",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.95,
"importanceConf": 0.94,
"brandDeal": 0.01,
"scam": 0.42
},
{
"id": "19ff2369dd071170",
"date": "2026-08-11",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.94,
"importanceConf": 0.94,
"brandDeal": 0.01,
"scam": 0.43
},
{
"id": "19ff170e3538fc02",
"date": "2026-08-11",
"from": "portraits-noreply@google.com",
"subject": "Portraits is shutting down on September 14, 2026",
"category": "service_notice",
"categoryConf": 1,
"importance": 1.47,
"importanceConf": 0.61,
"brandDeal": 0.01,
"scam": 0.22
},
{
"id": "19fe0568e97bad68",
"date": "2026-08-08",
"from": "CloudPlatform-noreply@google.com",
"subject": "50% of budget reached",
"category": "receipt",
"categoryConf": 0.94,
"importance": 2.68,
"importanceConf": 0.5,
"brandDeal": 0.01,
"scam": 0.28
},
{
"id": "19fd77b5b731bc69",
"date": "2026-08-06",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.9,
"importanceConf": 0.91,
"brandDeal": 0.01,
"scam": 0.3
},
{
"id": "19fd78662684bf00",
"date": "2026-08-06",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.91,
"importanceConf": 0.93,
"brandDeal": 0.01,
"scam": 0.29
},
{
"id": "19fbe7509db41cd0",
"date": "2026-08-01",
"from": "payments-noreply@google.com",
"subject": "Google: We've received your payment for [redacted]",
"category": "receipt",
"categoryConf": 1,
"importance": 0.84,
"importanceConf": 0.87,
"brandDeal": 0.01,
"scam": 0.17
},
{
"id": "19f9e0e42d8a044e",
"date": "2026-07-26",
"from": "you@gmail",
"subject": "Summary of chat sessions from the last five hours",
"category": "other",
"categoryConf": 0.78,
"importance": 0.96,
"importanceConf": 0.88,
"brandDeal": 0.01,
"scam": 0.13
},
{
"id": "19f96bcce5c083d1",
"date": "2026-07-25",
"from": "googleplay-noreply@google.com",
"subject": "Your Google Play Order Receipt from Jul 24, 2026",
"category": "receipt",
"categoryConf": 1,
"importance": 1.67,
"importanceConf": 0.52,
"brandDeal": 0.01,
"scam": 0.2
},
{
"id": "19f6dc4d38966096",
"date": "2026-07-17",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.95,
"importanceConf": 0.94,
"brandDeal": 0.01,
"scam": 0.46
},
{
"id": "19f6dc65035dcfde",
"date": "2026-07-17",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.94,
"importanceConf": 0.94,
"brandDeal": 0.01,
"scam": 0.5
},
{
"id": "19f6dc6a0bf0b09c",
"date": "2026-07-17",
"from": "no-reply@accounts.google.com",
"subject": "Security alert",
"category": "security_alert",
"categoryConf": 1,
"importance": 3.95,
"importanceConf": 0.94,
"brandDeal": 0.01,
"scam": 0.47
},
{
"id": "19f68b0f10462e50",
"date": "2026-07-16",
"from": "you@yahoo",
"subject": "contract",
"category": "business_inquiry",
"categoryConf": 1,
"importance": 2.79,
"importanceConf": 0.81,
"brandDeal": 0.07,
"scam": 0.35
},
{
"id": "19f675333f10c928",
"date": "2026-07-15",
"from": "googledevelopers-noreply@google.com",
"subject": "[Action Advised] Manage your unused OAuth clients and protect your information",
"category": "service_notice",
"categoryConf": 0.56,
"importance": 2.88,
"importanceConf": 0.79,
"brandDeal": 0.01,
"scam": 0.46
},
{
"id": "19f66180662d15f9",
"date": "2026-07-15",
"from": "you@yahoo",
"subject": "call [redacted]",
"category": "personal_reminder",
"categoryConf": 0.93,
"importance": 3.36,
"importanceConf": 0.67,
"brandDeal": 0.01,
"scam": 0.79
},
{
"id": "19f661703593e7db",
"date": "2026-07-15",
"from": "you@yahoo",
"subject": "check tire",
"category": "personal_reminder",
"categoryConf": 0.97,
"importance": 2.95,
"importanceConf": 0.94,
"brandDeal": 0.01,
"scam": 0.29
},
{
"id": "19f661609cded48f",
"date": "2026-07-15",
"from": "you@hotmail",
"subject": "AI subscriptions",
"category": "personal_reminder",
"categoryConf": 0.97,
"importance": 3.06,
"importanceConf": 0.86,
"brandDeal": 0.02,
"scam": 0.55
},
{
"id": "19f66141392887bc",
"date": "2026-07-15",
"from": "you@hotmail",
"subject": "Library book due",
"category": "personal_reminder",
"categoryConf": 0.36,
"importance": 3.65,
"importanceConf": 0.77,
"brandDeal": 0.01,
"scam": 0.22
},
{
"id": "19f1f0332f4712c6",
"date": "2026-07-01",
"from": "payments-noreply@google.com",
"subject": "Google: We've received your payment for [redacted]",
"category": "receipt",
"categoryConf": 1,
"importance": 0.83,
"importanceConf": 0.86,
"brandDeal": 0.01,
"scam": 0.17
},
{
"id": "19f0565c95cd1660",
"date": "2026-06-26",
"from": "google-noreply@google.com",
"subject": "Learn more about our updated Terms of Service",
"category": "service_notice",
"categoryConf": 1,
"importance": 0.79,
"importanceConf": 0.86,
"brandDeal": 0.01,
"scam": 0.14
},
{
"id": "19efc3e0bdf315ac",
"date": "2026-06-25",
"from": "workspace-noreply@google.com",
"subject": "Your Google AI subscription and Google Workspace Experiments terms",
"category": "service_notice",
"categoryConf": 1,
"importance": 1.93,
"importanceConf": 0.54,
"brandDeal": 0.02,
"scam": 0.29
},
{
"id": "19efc3e002bc2442",
"date": "2026-06-25",
"from": "googleplay-noreply@google.com",
"subject": "Your Google Play Order Receipt from Jun 24, 2026",
"category": "receipt",
"categoryConf": 1,
"importance": 1.73,
"importanceConf": 0.49,
"brandDeal": 0.01,
"scam": 0.2
}
]
The table the script printed, sorted by importance
imp conf scam deal category date from subject
3.95 0.95 0.40 0.01 security_alert 2026-08-11 no-reply@accounts.google.com Security alert
3.95 0.94 0.42 0.01 security_alert 2026-08-11 no-reply@accounts.google.com Security alert
3.95 0.94 0.46 0.01 security_alert 2026-07-17 no-reply@accounts.google.com Security alert
3.95 0.94 0.47 0.01 security_alert 2026-07-17 no-reply@accounts.google.com Security alert
3.94 0.94 0.43 0.01 security_alert 2026-08-11 no-reply@accounts.google.com Security alert
3.94 0.94 0.50 0.01 security_alert 2026-07-17 no-reply@accounts.google.com Security alert
3.93 0.94 0.41 0.01 security_alert 2026-09-10 no-reply@accounts.google.com Security alert
3.92 0.93 0.41 0.01 security_alert 2026-09-10 no-reply@accounts.google.com Security alert
3.92 0.93 0.42 0.01 security_alert 2026-09-10 no-reply@accounts.google.com Security alert
3.92 0.93 0.44 0.01 security_alert 2026-09-10 no-reply@accounts.google.com Security alert
3.91 0.93 0.29 0.01 security_alert 2026-08-06 no-reply@accounts.google.com Security alert
3.90 0.91 0.30 0.01 security_alert 2026-08-06 no-reply@accounts.google.com Security alert
3.88 0.90 0.30 0.01 security_alert 2026-08-31 no-reply@accounts.google.com Security alert
3.88 0.90 0.30 0.01 security_alert 2026-08-31 no-reply@accounts.google.com Security alert
3.87 0.90 0.32 0.01 security_alert 2026-08-31 no-reply@accounts.google.com Security alert
3.65 0.77 0.22 0.01 personal_reminder 2026-07-15 you@hotmail Library book due
3.36 0.67 0.79 0.01 personal_reminder 2026-07-15 you@yahoo call [redacted]
3.06 0.86 0.55 0.02 personal_reminder 2026-07-15 you@hotmail AI subscriptions
2.98 0.67 0.40 0.01 service_notice 2026-08-12 googleaistudio-noreply@googl [Action Required] Update your Gemini API billing in Google A
2.95 0.94 0.29 0.01 personal_reminder 2026-07-15 you@yahoo check tire
2.90 0.52 0.38 0.01 security_alert 2026-09-07 no-reply@accounts.google.com Critical security alert
2.88 0.79 0.46 0.01 service_notice 2026-07-15 googledevelopers-noreply@goo [Action Advised] Manage your unused OAuth clients and protec
2.81 0.77 0.45 0.01 service_notice 2026-08-25 googledevelopers-noreply@goo [Action Advised] Manage your unused OAuth clients and protec
2.79 0.81 0.35 0.07 business_inquiry 2026-07-16 you@yahoo contract
2.74 0.55 0.36 0.01 service_notice 2026-08-18 alerts@notify.bank.example Bank Change to Your Delivery Preference
2.68 0.50 0.28 0.01 receipt 2026-08-08 CloudPlatform-noreply@google 50% of budget reached
1.93 0.54 0.29 0.01 service_notice 2026-06-25 workspace-noreply@google.com Your Google AI subscription and Google Workspace Experiments
1.73 0.49 0.20 0.01 receipt 2026-06-25 googleplay-noreply@google.co Your Google Play Order Receipt from Jun 24, 2026
1.70 0.50 0.20 0.01 receipt 2026-08-25 googleplay-noreply@google.co Your Google Play Order Receipt from Aug 24, 2026
1.67 0.52 0.20 0.01 receipt 2026-07-25 googleplay-noreply@google.co Your Google Play Order Receipt from Jul 24, 2026
1.51 0.63 0.19 0.01 security_alert 2026-09-18 noreply-accounts@google.com You shared some Google Account data with typesafe.ai
1.47 0.61 0.22 0.01 service_notice 2026-08-11 portraits-noreply@google.com Portraits is shutting down on September 14, 2026
1.42 0.61 0.42 0.01 newsletter 2026-08-12 googleaistudio-noreply@googl Upgrade your Gemini 2.5 Flash workloads
1.34 0.73 0.19 0.01 security_alert 2026-08-18 noreply-accounts@google.com You shared some Google Account data with DocuSign Demo
1.07 0.91 0.26 0.01 newsletter 2026-08-20 contact@alphaxiv.org Introducing Researchers To Follow
0.96 0.88 0.13 0.01 other 2026-07-26 you@gmail Summary of chat sessions from the last five hours
0.91 0.89 0.17 0.01 receipt 2026-08-13 payments-noreply@google.com Google: We've received your payment for [redacted]
0.89 0.90 0.16 0.01 receipt 2026-09-01 payments-noreply@google.com Google: We've received your payment for [redacted]
0.84 0.87 0.17 0.01 receipt 2026-08-01 payments-noreply@google.com Google: We've received your payment for [redacted]
0.83 0.86 0.17 0.01 receipt 2026-07-01 payments-noreply@google.com Google: We've received your payment for [redacted]
0.79 0.86 0.14 0.01 service_notice 2026-06-26 google-noreply@google.com Learn more about our updated Terms of Service
Cost and time
| Item | Value | Source |
|---|---|---|
| Calls to Jev | 42 (1 raw run, then 41) | Counted |
| Market cost per call | $0.000029778 | providerMetadata.gateway.marketCost, first call |
| Market cost, whole session | about $0.00125 | 42 × the figure above; per-call cost varies with snippet length |
| Handoff's estimate | $0.02 per 500, so $0.00004 per email | HANDOFF.md, previous session |
| Provider time, first call | 166 ms | endTime − startTime |
| Wall clock, 41 calls sequential | not timed | The call returned inside its 300-second timeout |
| Rate limits hit | None | Paid tier; no 429 in 42 calls |
7. The crux — Jev saw 100 characters and has no clock
The run had no error. Every row got four answers. The crux is what the answers say about the input. Four rankings are wrong for the user, and each one traces to something Jev was not given.
Mis-rank 1: fifteen routine alerts above the one real one
The 15 OAuth-consent "Security alert" rows scored 3.87 to 3.95 on importance, with confidence 0.90 to 0.95. The one "Critical security alert" about passwords found in a data breach scored 2.90 with confidence 0.52. The user needs the second one and can ignore the first fifteen; they confirm consents the user gave to Muse, Activepieces, Cursor, Zapier, and Grok Business.
Mechanism: the OAuth snippet contains "If you didn't allow … access to some of your Google Account data, someone else may be trying to access your account." The breach snippet contains "Your Google Account is not affected." Jev scores the text it is given. One text describes a threat; the other says there is none. Jev has no way to know that the first text is boilerplate under every consent.
Fix that does not need Jev: pass the thread's message count in the state ("this is message 3 of 4 in a thread with identical subjects"), or collapse a thread to one row before the call. Fix inside the question: add a level string that names the pattern, for example "high: a security notice that reports a breach, a new device, or a password exposure". The current "high" says "needs an action or a reply within a few days", which the boilerplate also matches.
Mis-rank 2: the user's own reminder flagged as a probable scam
"call [redacted]" from you@yahoo scored 0.79 on scam. "AI subscriptions" from you@hotmail scored 0.55. The OAuth alerts, which contain phishing-shaped text, scored 0.29 to 0.50. The Google receipts scored 0.16 to 0.20.
Mechanism: the reminder snippet is "please reply if you receive this reminder: you are many months behind contacting [redacted]. act on it". That is a short imperative demand with a reply request, from an address Jev has never seen. The scam instruction says "looks like a scam, phishing, or something untrustworthy". The text matches the description. Jev was not told that you@yahoo is the user.
Fix: put an allow-list in the state. One extra key, from_known_contact: true, computed in code from a list of the user's own addresses, gives Jev the signal it lacks. This is the "state carries the meaning" pattern from the router: the instruction cannot say "trusted senders", but the state can.
Mis-rank 3: a two-month-old deadline scored as urgent
"Library book due", dated 2026-07-15, snippet "will be due in 24 hours", scored 3.65 on 2026-09-18. "check tire" dated 2026-07-15 with a deadline of "this Sat. 7/18/2026" scored 2.95. Both deadlines had passed 62 days before the run.
Mechanism: Jev has no clock. The state carried date: "2026-07-15", but nothing carried today's date. "Due in 24 hours" is urgent text regardless of when it was written. This finding was not in the agent's first report to the user. The advisor's third consultation, before this document, pointed it out.
Fix in code: compute the age and pass it, for example age_days: 65, or sent: "65 days ago". Then the "critical" level string can say "a deadline that has not yet passed". Or floor the importance of any row older than N days after the call, with no Jev change.
Mis-rank 4: a test email read as a business inquiry
"contract" from you@yahoo, snippet "we are interested in signing a 10-year contract with you.", scored business_inquiry with confidence 0.81, importance 2.79, brand deal 0.07. It is a test message the user sent to themself. Jev cannot tell a test from a real inquiry, and should not be expected to. The same allow-list key from mis-rank 2 would let a rule in code downgrade it.
What was right
All 41 categories are plausible on inspection. The receipts and the terms-of-service notice sit at the bottom, 0.79 to 0.91. The two newsletters sit at 1.07 and 1.42. The brand-deal probability is 0.01 on 39 rows, 0.02 on 2 rows, and 0.07 on the test "contract" row. The billing "[Action Required]" notice with a deadline of 2026-10-12 sits at 2.98, above the receipts and below the reminders. The confidence values track the disagreement: the breach alert's 0.52 is the lowest importance confidence in the top half, and it is the row Jev got most wrong.
Rule learned: every mis-rank was a missing input, not a wrong model. Before you change a question, list what the state does not carry: today's date, who the user is, how many times this arrived, what the body says.
8. Tools and features used
| Tool | What it did in this session |
|---|---|
Read, Bash cat/grep/sed -n | Read HANDOFF.md, server.mjs, probe.mjs, package.json, and the transcript notes in .ignore/ to find Riley's exact category list and score scale (transcript lines 74 to 92). |
| ToolSearch | Loaded the schemas for search_threads, get_message, get_thread, and WebFetch in one call. The search_threads schema is where pageSize max 50 and the in:inbox operator were read. |
Gmail MCP search_threads | One call. 31 threads, 43 messages. The only Gmail call of the session. get_message and get_thread were loaded and never called. |
| WebFetch | Four fetches: llms.txt (page index), primitives/score.md (array criteria, fractional score), primitives/noul.md (direct-API shape, answer field noul), patterns/composite-scoring.md (no multi-type example; the fetch said so). |
| Write | .ignore/emails.json (hand-typed from the tool result), triage.mjs, the template and build script for this page in the session scratchpad. |
Bash node --env-file=.env | The --limit 1 --raw run, then the full run. A sed -i between them removed the three-way fallback on the boolean field. |
| Edit | HANDOFF.md twice, the memory file once. |
| AskUserQuestion | One round, for this document: publish target and extra redactions. The user chose public and full redaction. |
| advisor | Three consultations. Before the build: insisted on a raw run before writing the boolean extraction, on filtering SENT-only rows, on keeping data under .ignore/, and on the other bucket. Before the first report: added the git status boundary check and the memory update, and the cost figures as two numbers. Before this document: added the no-clock finding, the detached-HEAD check, the complete redaction list including the address inside the summary snippet, and the unexplained cost: "0". |
| Skill: dev-journey | This document. |
No subagents. All work ran inline on Fable 5.1.
Human-in-the-loop moments
- The user answered the label and range question: "1 INBOX 2 last 90 days".
- The user's Gmail connector had to be authorized in claude.ai before the session. It was. The agent did not have to do anything.
- The user asked how to test the triage and how to run it on 30 days. Both were answered as questions, with no code change.
- The user chose "public, docs/triage.html" and "redact all of them" for this page.
9. What went wrong, and the fixes
No tool returned an error this session. The warts are in the process.
- The boolean answer field was unknown. WebFetch summaries of the score and Noul pages returned the direct-API shapes. The Noul page says the answer field is
noul. The gateway calls the typeboolean. No page said what field the gateway returns. The firsttriage.mjscarrieda.scam.probability ?? a.scam.value ?? a.scam.noul ?? null. The raw run showedprobability. Ased -icut the fallback toa.scam.probability. Had the raw run been skipped, the wrong field would have produced 41 nulls that print as-, and the report would have said "no scams found". - A hand-typed input file. Section 3. The tool result was retyped, not mapped. Not fixed. Named here as the largest reproducibility risk in the session.
- Duplicates were scored one by one. 15 of 41 rows are the same consent alert repeated inside five threads (4 Muse, 3 Activepieces, 3 Cursor, 2 Zapier, 3 Grok Business). Each cost a call and each took a top slot in the table. No thread-level dedupe was done. The fix is one line before the loop or one key in the state; section 7 has both.
- A finding missed in the first report. The report to the user named two mis-ranks (the breach alert, the scam score). It did not name the stale-deadline rows. The advisor caught it while preparing this document. The report was correct in what it said and incomplete in what it did not.
- The 43-versus-41 count. The search returned 43 messages. The agent's first message to the user said "41 INBOX messages" without saying two were dropped. This page says so.
- The cost estimate was off by a factor of 1.3. The handoff said about $0.00004 per email. Measured: $0.00003. Small, and in the cheap direction, but the estimate had no basis; the measured figure does.
- The
categoryConfcolumn was never printed. The script stores it in the output file and omits it from the table. The table shows importance confidence only. Not fixed; the file has it.
Rule learned: a fallback chain like a ?? b ?? c ?? null in an extraction is a guess dressed as code. Print the raw body once and write the one true field.
10. Verification
- The gateway path works for four questions in one call: the raw body in section 6,
statusCode: 200, four answers present. - All 41 rows have all four answers: the table has no
-cell, which is what the formatter prints fornull. - Confidence present where the gateway provides it: every row has a numeric
importanceConfin the table and a numericcategoryConfin the output file. No confidence exists for booleans; that is the gateway's shape, not a gap in the run. - Redaction on this page: the build script searched the finished HTML for nine original strings and would have thrown on any hit. It did not throw.
- The data files are outside the repo:
git status --shortafter the run showed only?? triage.mjs..gitignorelists.ignore/.
Not verified
- The transcription of
emails.jsonagainst the tool result, character by character. - Any answer against the full email body. No body was fetched.
- Whether a rerun gives the same numbers. No second run was made.
- The meaning of
cost: "0"on a paid account. - The test file proposed to the user in the "how can I test" answer (a fake sponsorship, a fake phishing mail, a reminder, a newsletter). Described, not created, not run.
11. Unknown unknowns — and what to do about each
The router journey listed 18. These are the ones specific to triage. The third column is the fix or the habit.
| You might assume | What is true | What to do |
|---|---|---|
| The script fetches your Gmail. | It reads a JSON file. The Gmail MCP runs on claude.ai and answers only the Claude session. | Ask the agent to fetch and write the JSON. Or build a fetcher with the Gmail API and OAuth, about 60 lines plus console setup. |
| Jev read the emails. | It read Gmail's snippet: the first 100 to 230 characters, no body, no attachments, no headers beyond sender, subject, date. | Fetch bodies with get_message and messageFormat: "PLAIN_TEXT" for rows that matter, and put the body in the state. Watch the 32k-token state limit. |
| Jev knows what day it is. | It has no clock. "Due in 24 hours" from July scored 3.65 in September. | Compute age in code and put age_days in the state. Or floor old rows after the call. |
| Jev knows who you are. | It sees sender strings. Your own second address is a stranger. Your reminder to yourself scored 0.79 on scam. | Add from_known_contact to the state from an allow-list in code. |
| A security alert that sounds scary is important. | Consent boilerplate contains threat language. Fifteen consent alerts outranked the one breach alert. | Collapse threads to one row, pass the repeat count, or write a level string that names a breach, a new device, or a password exposure. |
| Every question gets a confidence. | Only choice and score do, under providerMetadata.typesafe.confidence. A boolean returns probability and nothing else. | Read the probability as the whole answer. Set your own thresholds on it. |
| A score is a level. | It is the probability-weighted mean of the level indexes. 1.49 means "mostly level 1, some level 2". The full distribution is in probabilities. | Use the distribution when the mean sits between two levels. Round only for display. |
| The level names are metadata. | The strings are the input. "critical: needs an action today; …" is one sentence to Jev, label included. | Write each level as a situation, not an adjective. Put the pattern you want matched in the string. |
| Four questions cost four calls. | One request carries all four. 42 requests for 41 emails plus one raw run. $0.00003 each at market cost. | Add questions freely. Remove emails, not questions, to save money. |
| Each message is one email. | search_threads returns threads; a thread holds messages with mixed labels. Two SENT-only replies sat inside inbox threads. Four identical alerts sat inside one thread. | Filter per message on labelIds. Decide per thread or per message before you count. |
| One search page is the inbox. | pageSize max is 50 threads. This inbox fit in one page. A busier one does not. | Loop on nextPageToken until it is absent. |
| The state is the snippet. | The state is an object. Its key names are input. from, subject, date, snippet all shape the answer. | Name keys the way a person would label the fields. Keep the same keys between runs, or numbers drift. |
| A Choice can abstain. | It always picks one. other caught one row here. Without it, the self-sent summary would have been forced into a wrong bucket. | Keep an other option. Watch its count; a large count means your list is wrong. |
| Jev saw your name in the criteria and knows you. | "[name]" in the instructions is a word. It links to nothing in the state. | Names in instructions do no harm and no work. The state must carry the identity signal. |
| The JSON in the repo is the data. | The data files are under .ignore/, listed in .gitignore, and unredacted. This page holds a redacted copy. Only the redacted copy is public. | Keep inbox data out of the tree. Check git status before every push. |
cost: "0" means free. | On a paid account the gateway still printed cost: "0" next to marketCost: "0.000029778". The reason is not in any page read. | Bill by marketCost until the credits endpoint or the pricing page says otherwise. |
| Jev ran over the inbox in real time. | It ran over a file typed by the agent from one tool result, later in the same session. New mail after the fetch is not in it. | Refetch before every run. Treat the JSON as a snapshot with a date. |
12. Where things stand
Done
triage.mjs: four questions per email over a JSON file. Committed with this page..ignore/emails.jsonand.ignore/emails.triage.json: the snapshot and the answers, unredacted, untracked.HANDOFF.md: updated with the run, the verified field names, and the findings.- Memory file
jev-gateway-facts.md: two facts added. Boolean answers areanswers.<id>.probability. The confidence map covers choice and score only. - This page,
docs/triage.html, on the same GitHub Pages site as the router journey.
Not built, by choice
- A Gmail fetcher in Node. Needs OAuth and a Google Cloud client. Not asked for.
- Body fetching. Snippets were enough to run the pipeline. The mis-ranks in section 7 are input problems that a body would partly fix.
- Thread dedupe, the allow-list key, the age key. Each is a few lines. None was asked for. Section 7 has the shape of each.
- Gmail labels written back. Riley's video ends with a filter that removes cold mail. This run wrote nothing to Gmail. The MCP has
label_messageandcreate_label; a write-back is one loop over the output file. - An HTML dashboard of the results. The table in section 6 is the deliverable.
Still yours
- Rotate the gateway key. It was printed into a session transcript two sessions ago. Still not rotated as of this page.
- Pin
"ai": "7.0.107"inpackage.json. Still^7.0.107.
Next options
- Add
age_daysandfrom_known_contactto the state. Rerun. Compare the four mis-ranks. - Fetch bodies for the rows with scam at or above 0.5 and rerun those rows.
- Run the four-email test file from the "how can I test" answer to check that the questions discriminate.
- Write labels back to Gmail from the output file, behind a dry-run flag.