Try it: https://challengeaccepted.app — sign in with Google · health: https://challengeaccepted.app/api/healthz (states the auth mode out loud, so "is it actually locked?" is answerable without taking our word for it)
Inspiration
Every AI I asked for help with a goal gave me a plan. Good plans. Usually, twelve numbered steps, sensibly ordered. I did none of them — because each step still needed something I didn't have: a spreadsheet model, a checklist, a comparison of four vendors, a practice set. No easy way to track what was done or to keep me focused.
My motivation isn't missing. The plan wasn't the missing piece. The tools were.
What it does
You name something you want to be true that isn't yet. A team of nine agents:
- Interviews you — 5–9 clarifying questions, and only questions whose answer would change the plan. It stops when the goal is checkable by a stranger.
- Draws a goal graph — 8–20 micro-tasks as a dependency DAG, each ≤2 hours, each with a written acceptance criterion. Not a list. Parallel work stays parallel.
- Builds the tools — for every node it asks "what tool would make this step trivial?" then writes it, runs it, smoke-tests it, and attaches it to the node. Calculators, checklists, drills, trackers, scripts, research briefs, mini-apps.
- Coaches you through it — one ready step at a time, verifying evidence against the acceptance criterion, capturing thumbs up/down on every tool.
Everything learned lands in goal-scoped group memory. When a teammate opens the same challenge, their Coach opens with what you discovered — attributed to you by name.
The tools remember, and some of them remember for everybody. A tool runs in a
sandboxed iframe with no storage of its own, so the dashboard brokers its state: seeded
in before the page runs, written back over postMessage, saved against your account. A
training log is yours. A cost split is the party's — the Quartermaster decides which at
build time, and a save that would overwrite a teammate's is refused with theirs
attached, naming who changed it. One record where one record is the point, one each
where it is not.
You sign in with Google, and the party roster shows the people on it rather than machine ids. Dark and light themes; the app follows your machine and remembers if you disagree.
How we built it
Nine agents on Google ADK, coordinated by a root agent called Warden:
warden (coordinator, gemini-3.6-flash)
├── interviewer mode=task ACCEPT clarifying questions -> charter
├── cartographer mode=single_turn MAP charter -> goal graph DAG
├── forge Sequential FORGE
│ ├── quartermaster single_turn per node: which tool is missing?
│ └── forge_loop Loop
│ ├── dispatcher custom BaseAgent deterministic slot assignment
│ └── forge_workers Parallel
│ └── toolwright_0..7 build + smoke-test, concurrently
├── coach mode=task CLIMB one node at a time
│ └── referee AgentTool evidence check + feedback
├── archivist mode=single_turn — takes the notes
└── scout AgentTool grounded search, on demand
Stack: Google ADK 2.6.3 · Gemini 3.6 Flash + 3.5 Flash-Lite · Gemma 4 (open
model, Vertex MaaS) · gemini-embedding-001 · Cloud Run · Firestore · Vertex AI Memory
Bank on Agent Engine · FastAPI.
Three model tiers, and the third one is open. Reasoning tier where judgement is
needed; Flash-Lite for bookkeeping; and gemma-4-26b-a4b-it-maas for the Archivist,
the one agent that only transcribes — it does not decide, plan, grade evidence or write
code, and its output is purely additive, so a weaker answer costs a thinner journal
entry rather than a broken phase. It rides the same Vertex credentials and the same
global endpoint as everything else: no API key, no deployed endpoint, no GPU. That was
the deciding factor — Gemma on the Gemini Developer API would have meant a second auth
surface, which is precisely what caused the outage described below. /api/healthz
names the serving model for each tier, so "is Gemma actually running?" is answerable
from outside without taking our word for it.
Three things that line does not claim, because they are not true and a stack list is the easiest place in a submission to lie by omission:
- Sessions are not Agent Engine sessions. They run on a
BaseSessionServicewe wrote against Firestore, registered under afirestore://scheme through ADK's own service registry. That was a deliberate build, not a fallback — see Challenges. - Code execution is ADK's
BuiltInCodeExecutor, model-side, not a persistent Agent Runtime sandbox. One env var swaps it; the env var is unset. - Sign-in is Google only. Firebase Authentication, verified server-side with the Admin SDK. There is no email/password tier and no anonymous tier — an anonymous uid is the same fiction we removed, wearing a server-issued costume.
Identity, and what it actually protects
Until late in the build, identity was a random string the browser generated and the
server believed. The roster listed u_9a3d0a; a challenge belonged to whichever group
the client claimed; anyone who guessed a challenge id could read someone else's plan.
Adding a login is the easy half. The half that matters is that ADK takes the user id
from the URL (POST /apps/{app}/users/{user_id}/sessions) and from the /run_sse
body — and those ids are what save_charter files a challenge under. Verifying a
token and then letting the client keep naming itself would be theatre: you could sign in
as yourself and create a challenge owned by someone else, with a token that verifies
perfectly. So the gate refuses any request whose stated user is not the verified one,
and there is a test for each path.
Reading a challenge requires membership of its party, not possession of its id. An invite link is an invitation to join — sign in, join, then see — and joining is a button you press, not something a URL does to you.
The ADK dev UI is switched off whenever auth is on. It is an unauthenticated console that can run agents as any user id you type into it, which on a public URL is a free Gemini endpoint on our bill. A gate with a second door beside it is a decoration.
Data sources: user conversation, Google Search grounding (gated, not default-on), and the group's own accumulated memory.
Decisions worth defending
Flash-only, deliberately. The rules require "Gemini 3.5 or newer." As of August 2026
that is satisfied only by the 3.5/3.6 Flash family — there is no Gemini 3.5 Pro, and
gemini-3.1-pro-preview doesn't qualify despite being the flagship. So the architecture
compensates with structure rather than model power: two-pass decomposition, a closed
seven-type tool taxonomy, typed contracts between every phase. Bookkeeping agents run on
Flash-Lite at 5x lower input cost.
The Dispatcher is a custom BaseAgent, not an LlmAgent. Assigning queue items to
worker slots is deterministic bookkeeping; a model call there would be slower, less
reliable, and untestable. Because it isn't a model, it runs through a real ADK Runner in
the test suite with no API key — so the fan-out is the best-covered code in the repo.
Firestore is the source of truth, not session state. ADK's ParallelAgent docs are
explicit that branch state isn't automatically shared during execution. Session state is
only ever a per-slot inbox that exactly one worker reads.
A closed tool taxonomy is scope control. Seven types, nothing else. Open-ended codegen is the single biggest live-demo risk; a failed build degrades to a plain checklist rather than blocking the graph.
Challenges we ran into
Every one of these was found by running the system. None were reachable from tests that don't call a model — which is the actual lesson.
400: enable tool_config.include_server_side_tool_invocations. A code executor is a server-side tool and Gemini won't mix it with client-side function calling unless that flag is set. Every Toolwright 400'd, andParallelAgentburied it in anExceptionGroupso it surfaced only as "unhandled errors in a TaskGroup."- An infinite delegation loop. The Referee was a
task-mode sibling. The Coach couldn't reach it, bounced to Warden, and Warden calledreferee(...)25 times in one turn, each returning nothing. Fixed by making it anAgentToolthe Coach holds. AgentToolagents must bemode="chat"— an AgentTool runs its agent as a root, and ADK rejects a non-chat root. Scout had the identical defect sitting latent because nothing had ever invoked it.- Silent spec loss on re-entry. The Dispatcher seeded its queue only when it was
None; on a second FORGE entry the drained queue was treated as authoritative and every new ToolSpec was discarded — no error, no output, just nothing built. - Workers re-reported instead of idling. Sharing a branch across loop iterations, a
worker saw its own prior turn and wrote confident prose about a tool it had already
built, with no
save_toolcall. - Re-planning appended instead of replacing. A redraw after a blocker left 24 nodes — the old plan and the new one side by side.
- Token accounting was wrong. Thinking tokens bill as output on Gemini 3.x and live
in
thoughts_token_count, separate fromcandidates_token_count. Summing candidates alone under-reported cost by up to 3x. - A request that could not fail, and therefore told us nothing. The dashboard created
its ADK session by posting
{"state": {...}}toPOST .../sessions/{id}. That endpoint declaresstateas a bare body parameter — the whole body is the state — so every session was created with state equal to{"state": {...}}, one level too deep. FastAPI returned200 OK. Nothing logged. Three unrelated-looking symptoms followed from it: every challenge created from the dashboard was owned byanonin groupgrp_anon, a teammate opening an invite link resolved to their own private group, and the Warden re-interviewed people about a goal already drawn on the screen behind the chat panel. Found only by driving two real browsers at once. - An agent claiming a write it had no tool to perform. Warden answered a teammate with
"I've recorded that into our shared group memory." Nothing was written —
remember_group_factwas on Archivist and Coach, not on Warden. It was the obviously right sentence to say, so the model said it. An agent that can claim a thing must be able to do the thing. - A rule that lost an argument with seven other rules. "A joining teammate does not get
interviewed" sat eighth in a numbered list and was ignored live. Rewritten as an ADK
InstructionProviderthat restates the in-flight challenge — title, outcome, progress — as fact each turn. Rules compete. Facts don't. - A feedback button that wrote to a table nobody read.
record_feedbackhad no reader anywhere in the codebase, so "tell it what didn't work and the next one is different" was false. It had gone unnoticed because the reason box was a nativewindow.prompt(), which blocks the page and every browser check driving it. An untestable control rots. - The money shot had never once run on the deployed service. Not degraded — zero
tools, on every revision, while local runs built four to six every time and our own
README recorded that as verified. Three bugs, stacked, each hiding the next, and every
one of them invisible from outside: deploy green, health green, graph drawn, journal
filling, FORGE rail animating around
tools: [].
(1) include_server_side_tool_invocations is required on the Gemini Developer
API and raises on Vertex. Local runs use an API key; the deploy uses Vertex. The
fix for one environment was the outage in the other. That took us from 0 tools to
exactly 1 — always exactly 1, which is the shape of a bug, not a shortfall.
(2) A worker's first model call returns an executableCode part carrying an id.
ADK keeps it in history; the second call sends it back; google-genai refuses to
convert it. And workers run in an asyncio.TaskGroup, where one failure cancels its
siblings — so the first worker to reach a second call killed the other three
mid-build. Hence exactly one tool, from whoever finished first.
(3) With all four surviving, it still built 4 of 7. include_contents="none"
filters session history; it does not clear what the flow accumulates inside one
invocation, and the LoopAgent re-runs the same worker instances. On iteration two the
worker saw the tool it had already built and wrote prose about it in 1.6 seconds
instead of building the new one — the exact failure that flag was added to prevent.
Now 6 specs asked, 6 tools built, on the deployed service. What made any of it findable was refusing to read silence as success: a worker that does nothing and a worker with nothing to do produce identical output, so the phase now narrates itself — dispatcher assignments, per-worker START/END with slot contents, and every model call with its instruction size and content count. One env var, off by default.
- The FORGE regression that was never about concurrency. The same shortfall came
back — 5 specs, 2 tools; 6 specs, 1 tool — and the obvious suspect was worker
parallelism, so we halved the batch. It got better. Three times running. That is
exactly what a race looks like and exactly what a narrowed window looks like, and we
could not tell them apart by staring at the code, so we stopped and built the tooling
instead: a flow tracer that logs every agent entry with its branch, every tool
call, and every
transfer_to_agent. The answer was in the first trace.
branch=cartographer@call_636196.forge_workers.toolwright_0 <- 5 specs, 2 tools
branch=cartographer@call_630649.forge_workers.toolwright_0 <- 6 specs, 1 tool
branch=forge_workers.toolwright_0 <- 8 specs, 8 tools
FORGE was running inside the Cartographer's frame. A mode="single_turn" sub-agent
is not a transfer target in ADK — the parent gets it as a tool, run in an isolated
sub-branch. But ADK still offers that child its parent as a transfer target, and when
the child takes it, the Warden resumes inside the child's sub-branch rather than
after it. Everything the Warden starts from there — the entire build phase — is
parented to a tool call that is about to return. When it does, FORGE dies underneath
it: workers issue a second model call, never reach after_agent_callback, the loop
never dispatches another batch, and nothing logs an error. Two lines fixed it
(disallow_transfer_to_parent, disallow_transfer_to_peers — finish and return, like
the tool it already was) and the workers went back up to 8. The lesson is the one we
keep re-learning: a change that makes a symptom go away three times running is still
not a diagnosis.
A prompt that instructed the exact behaviour that hangs the framework. A run failed with 0 specs and 0 tools, and the trace showed the whole story in two lines:
enter interviewerat 22:25:23,leave interviewerat 22:25:35, and then nothing — noenter warden, no Cartographer, no FORGE, no error. The Interviewer's prompt ended with "tell the user in one sentence what you heard, and finish." Amode="task"agent that emits text instead of callingfinish_taskdoesn't finish; ADK pauses and waits for a user reply that is never coming. We had written the stall into the instructions ourselves, in the most natural-sounding sentence in the file. The prompt now demandssave_charterandfinish_taskin the same turn and says why in-line, so the next person to make it friendlier reads the reason first.Three unrelated subsystems riding on one boolean.
use_vertex()gated sessions, memory and Cloud Trace. Switching on Memory Bank would therefore also have moved every conversation off our Firestore session service onto Agent Engine — nothing would have errored; sessions would just have been served by code with no coverage in this repo. Splitting it exposed a second trap by reading ADK's source: given a bare engine id, itsagentengine://factory reads the region fromGOOGLE_CLOUD_LOCATION, which we set toglobalbecause Gemini 3.x is served from the global endpoint. An Agent Engine is regional. That would have built a Memory Bank client aimed at a region the engine doesn't live in — and failed silently, because both ends swallow their errors by design, so the app would have run perfectly and remembered nothing. We emit the full resource path instead. The third only surfaced on deploy:trace_to_cloudturned on too, and its OpenTelemetry exporter had never been inrequirements.txt, so the container died at import before binding the port. The previous revision kept serving and the deploy script's explicit$LASTEXITCODEcheck reported the failure rather than printing "Deployed" over it. Three subsystems, found three different ways: reading the diff, reading the vendor's source, reading a crash log.The same bug came back, because the fix was on the wrong agent. The Vertex
id-on-code-parts crash returned weeks later, this time attributed towardenand only ever at the end of a FORGE turn. The guard was on the Toolwrights — the agents that execute code — which felt obviously right and was wrong. ADK segregates history per agent by comparing branch paths, and_is_event_belongs_to_branchopens withif not invocation_branch or not event.branch: return True. The root agent has no branch. So Warden does not get a filtered view of the conversation; it gets everything, including every parallel Toolwright'sexecutableCodeevents from deep inside FORGE, each carrying anidVertex refuses. The next time Warden spoke after a build, its own request contained them. Intermittent, invisible from the UI, and costly: one recorded run built a single tool where the run before it built seven. The traceback was what settled it —runners.py:610 _drive_root_node→_llm_agent_wrapper.run_llm_agent_as_node, which is the root being driven, not a worker. The guard now installs itself across the whole agent tree and the install count is asserted, because a silent no-op there restores the bug exactly.A workaround for a problem the framework had already solved, which took the site down. To check that
/run_ssewas not being run as somebody else, the auth gate has to read the request body. Assuming that consuming it would starve the route downstream, we hand-wrote a replacementreceive()that hands the body back — on every call./run_sseanswers with a streaming response, and Starlette sits in a loop onreceive()waiting for the client to disconnect. It got the body a second time and raisedRuntimeError: Unexpected message received: http.requestafter the HTTP 200 had already gone out. Every agent run on the site died at 46 ms with zero events and no error the user could see: you typed a message and nothing happened. Starlette caches and replays the body itself; the fix was deleting our code. The unit test had passed because it exercised a JSON route, and a JSON route never listens for a disconnect — it asserted the mechanism we had built rather than the thing the product does.Auth silently turned every invite link into a blank page. One line decided which quest a browser lands on:
if (!known(challengeId)) challengeId = null;. Correct while/challengesreturned every quest in the store — and the moment the list became private, a teammate opening an invite link had the id cleared on load. They saw "no quest yet", no map, and no offer to join, because the request that triggers the join prompt was never made. The entire collaborative premise, dead, with nothing anywhere saying so. Found by two signed-in browsers, not by reasoning about the diff.Three test failures that were the test's fault, not the product's. A browser walkthrough reported
nodes: 0and "FORGE produced nothing" against a service that had just built seven tools. It was waiting for a spinner the app removes when a response starts streaming, then screenshotting a run still in flight. A second check asserted on a dashboard shape it had invented. A third asserted that no tool could be opened from a detail pane it had never opened. Each one looked like a product bug and cost real time — the discipline that eventually pays is asking "is the measurement wrong?" before rewriting the thing being measured.We audited the checks themselves, and most of what they were reporting was noise. Having been burned three times, we stopped trusting the suite and ran every check against the deployed service one at a time, treating each failure as a claim to be proved rather than a bug to be fixed. Six were wrong about a working product: one reported NO HANDOFF against a correct handoff; one claimed tool state didn't persist when it did; one graded the Referee's correct refusal of weak evidence as a bug — twice, because the first fix hardcoded one sport's idea of evidence; one encoded the access model from before invite keys existed and snapshotted the page before the dashboard arrived; two hardcoded a worker count that had since changed. One had been blind since the day we added sign-in — every request 401'd and the check printed the traceback and moved on, which is the second time this repo has shipped a check that cannot fail. And the checks were hiding exactly one real defect: on any
/inviteerror the button copied a keyless link to the clipboard and said "Copied", so the failure mode of sharing a party was handing someone a link guaranteed to 403. A check that is wrong about a working product is worse than no check, because it spends the attention you were saving for the real one.
Accomplishments that we're proud of
- An agent that writes a tool, executes it, iterates until its own smoke test passes, and hands it to a human who isn't a programmer.
- Group memory that actually crosses users: a teammate's first message is "Derek found Cloud Run requires billing enabled and nobody on the team has admin, so we're using Vercel instead" — attributed, unprompted.
- A blocker re-opening the interview and redrawing the graph around the constraint, then building new tools for the new plan.
- A second person opening an invite link inherits the party's knowledge and adds to
it, with the header roster going 1 → 2 on the first person's screen while they sit
still. Driven through two separate browser contexts, not two tabs — tabs share
localStorageand would have let three real bugs pass. - Memory that survives the conversation it was told in. A brand-new session — no
challenge_id, nogroup_id, nothing in state — answered "what do you already know about me?" with a constraint the user had mentioned during a previous challenge. Vertex AI Memory Bank is read by ADK'spreload_memoryon Warden and the Interviewer and written bysave_charterandcomplete_node.scripts\check_memory.pyproves it end to end;scripts\check_memory_bank.pysits underneath and round-trips the service directly, so a failure tells you whether the infrastructure or the prompt broke. - The party notebook fills itself from the interview.
remember_group_factonly fires on a returning turn, so a freshly planned challenge greeted its first teammate with "Nothing learned yet" while the charter it had just written held the deadline, the constraints and everything already tried. Those now seed the notebook atsave_charter— in code, not by asking an agent, because a prompt that says "also record these" is a prompt that can claim it did and not have. Empty form answers are filtered: "None (running solo)" is not a shared discovery. - Dark and light, both measured. Every colour is a token, including the ones baked
into the SVG the quest map emits — left alone, the centrepiece would have stayed
near-black on a white page.
check_theme.pyreads the rendered luminance of every painted surface and fails if one stayed dark;check_a11y.pyruns contrast in both themes (worst: 5.60:1 dark, 4.86:1 light, against a 4.5:1 floor). - An open model doing real work, proved on the deployed service. The Archivist runs
on Gemma 4, and the risk was never quality — it was that a MaaS model might answer in
prose. The Archivist's job is to call
write_journalandremember_group_fact; a model that understands perfectly and replies "Sure, I've noted that down" gives you an agent that looks healthy, logs nothing, and takes the party's shared memory with it. So that question got asked before anything was wired up, against the live endpoint with the real tool schemas: 2 tool calls, no prose, 2.36s against Flash-Lite's 1.79s — 0.6s slower, between turns, in a phase nobody watches. Then the whole collaborative beat was re-run on production with Gemma serving: a stranger with an invite link inherited "Must host on Vercel due to GCP billing/admin restrictions" and was handed a live node. Backing it out is onegcloud run services update, no rebuild. - 256 tests plus 34 live checks that sign in, including a regression test for
every bug above. The checks click the actual controls and read the clipboard, the
iframe and the resulting prompt string back —
check_feedback.pyfollows a thumbs-down all the way into the Quartermaster's instruction, because "the loop is closed" and "the loop is open" look identical from outside. - Locking the doors did not cost us the ability to test them. Auth made every live
check return 401, which would have left the deployed service verifiable only by hand —
exactly the gap that let the streaming outage reach a user. The checks now mint a
Firebase custom token as an owner-level operation and exchange it for a real ID token;
the browser ones reach the page's own Firebase instance and sign in through it, so
everything after that line is the app a signed-in person actually uses. The only thing
skipped is the Google popup.
check_auth_live.pyruns the other way — it is the one check written from outside, where every assertion is something that must fail: no token, a forged-but-well-formed token, a real challenge id held as if it had leaked.
What we learned
Tests that never call a model cannot find the bugs that matter. Ours passed while the
system was silently building nothing. Every real defect came from a scripted live run with
full event logging — which is why scripts/live_walk.py, live_climb.py and
live_group.py sit in the repo alongside the unit tests.
The second lesson: when output is wrong, check whether the data the model needed even
exists before rewriting the prompt. Attribution failed not because the instruction was
weak but because the journal recorded "Archivist" as the actor — the user's name was
never stored anywhere.
The third arrived late and cost the most: a request that cannot fail cannot teach you
anything. A session payload shaped one level too deep returned 200 OK for weeks and
produced three symptoms that looked like three different bugs — a routing bug, a memory
bug, and a permissions bug. One malformed body. What finally exposed it was refusing to
accept an agent's word that it had saved something, and printing the tool calls the UI
actually recorded next to the sentence the agent said.
The fourth: a control nobody can automate is a control nobody notices has died. The
feedback button spent months writing rows that no code read, and the thing protecting it
from discovery was a window.prompt() that no browser test could get past.
The fifth, and the one we would tell another team first: check whether the framework
already solved it. The outage that cost us the most was not a hard problem badly
solved — it was an easy problem solved twice, where our version fought the library's.
Ten minutes reading starlette/middleware/base.py would have saved all of it. The same
lesson arrived from the other direction when a security change quietly broke a feature
two files away: scoping the quest list to its owner was correct, and it silently
invalidated the assumption an invite link depended on. A change is not finished when
the thing you changed works.
The sixth is the one that saved the most time once we finally did it: when two theories predict the same symptom, stop guessing and build the instrument. We spent days treating a FORGE shortfall as a concurrency race because halving the worker count improved it — which is equally consistent with narrowing a window you have not found. An afternoon spent writing a flow tracer that prints each agent's branch answered it in one run, and then went on to answer the next failure too. Instrumentation is not overhead you add after the bug; it is the thing that decides whether you get to have a diagnosis at all.
What's next for Challenge Accepted
Plans to intergrade with email and phone to email, text and call to motivate the user if goals are not being worked on.
Challenge Packs — every completed graph is a template. Publish it, take 30%. Then per-node assignment for teams, and a public gallery of generated tools.
Business model
Cost is roughly $0.86 per challenge created.
| Tier | Price | Included |
|---|---|---|
| Free | $0 | 1 challenge, 3 tools |
| Solo | $19/mo | 5 challenges, 40 tools/mo |
| Crew | $29/seat/mo | Unlimited, group memory, live graph |
| Forge | $99/mo | 400 tools, API, white-label |
Measured: one full challenge (12 nodes, 6 tools) = 243k prompt / 66k billed output ≈ $0.86 at 3.6-flash rates. A challenge that hits a blocker and re-plans runs ~$1.90. Break-even at $29/seat is ~34 challenges per user per month comfortably above real usage.
Known limitations, stated plainly
SequentialAgent/ParallelAgent/LoopAgentare deprecated in ADK 2.6.3 in favour ofWorkflow. We're on the deprecated path deliberately: the warning itself says "Workflow cannot yet be used as an LlmAgent sub-agent," andmode="task"agents are disabled inside graph-based workflows. Both ends of the migration are blocked.- Name collision: a UK company ships a consumer app called Challenge Accepted. We own challengeaccepted.app; the mark is unresolved and would need clearing before any commercial launch.
- Group memory is goal-scoped and works across users, but there is no per-node assignment or presence yet — two people can pick up the same node.
Anyone with an invite link can join, with no approval step and no way to remove them.Fixed. The link carries a rotatable key (?id=…&k=…): no key and a wrong key are both 403, any member can pass the link on but only the owner can reset it, and a reset kills the old key without touching anyone already on the roster. You can leave a party, and leaving revokes access to the dashboard, the tools and the journal — what you discovered stays, because your leaving does not make it untrue. An approval step was the other candidate and loses twice: it puts a human in the loop on the beat that has to be instant, and it still cannot un-send a link that has already gone.- Challenges created before sign-in existed are unreachable. They belong to anonymous localStorage ids that now map to nobody. We abandoned them rather than invent an owner for data we could not attribute.
- The party roster shows the name and avatar on your Google account. There is no display name of your own, and no way to appear as anything else.
- Memory Bank is personal recall, not the party's memory. It scopes to
(app_name, user_id), so it carries what you said between your challenges. A teammate joining a party does not inherit it. The shared layer isremember_group_fact→ Firestore. That used to be read wholesale into the prompt and would not have scaled past a few dozen facts; it is now ranked by meaning (256-d embeddings, cosine against the goal) and capped at 40, and the prompt carries the count it withheld so the model says "the team may know more" rather than "that is unknown". The honest remaining limit is measured, not guessed: an old fact sharing no words with the goal can still lose to newer noise, andcheck_fact_budget_live.pyreports that rather than asserting a guarantee the design cannot make. Calling Memory Bank "shared team memory" would be the easiest overclaim in this submission and we are not making it.
Built With
- css3
- docker
- fastapi
- firebase-admin
- firebase-authentication
- firestore
- gemini
- gemma
- google-adk
- google-cloud
- google-cloud-run
- google-genai
- html5
- javascript
- opentelemetry
- pydantic
- pytest
- python
- uvicorn
- vertex-ai

Log in or sign up for Devpost to join the conversation.