← All privacy impact assessments
PIA: Semantic task search — finding a task by meaning
Status: Built — written before the code, and revised against the shipped code the same day. The feature was built as designed: every mechanism below is live, the caps §3.2 asked for are numbers now (stated there), and the independent review of the build found no deviation from this assessment. A further change, opening an encrypted task for a search (§3.1), was built and merged on 30 September 2026 and has not yet been released. Written: 18 September 2026 · Last revised: 5 November 2026 (routine-edit lookup and its generative step, §9); previously 30 September 2026 (opening an encrypted task for a search, §3.1, built but not yet released) Nothing here is legal advice. Children's Code citations are to the exposure draft; section numbers may move on registration (due 10 December 2026).
As built, 30 September 2026. §3.1's change — reading a sealed task's title and notes for a search, inside the member's own request, once a task's words are sealed — has been built and merged, matching this assessment with no deviation we found. It has not reached anyone yet: task sealing itself is a separate change (see the content encryption assessment) that has not been released, so today every task's words are still stored in plain text and this change has nothing sealed to open.
1. Purpose and scope
A member looking for "that dentist thing" should be able to find the task "Ada — orthodontist referral" without remembering its exact words. The design is two search modes with a hard line between them:
- While typing: plain text matching over the tasks already on the device — titles and notes, case-insensitive, no network, no cost, no model. This is the default and it is always available, whatever the group's privacy settings.
- On an explicit submit (Return / Search): a semantic lookup — the query and the group's own task titles and notes are compared by meaning, using an embedding model, and the results are real, ranked task rows the member can already read. Nothing is generated, nothing is invented, and nothing happens on a keystroke.
This app already runs exactly one embedding surface — the schedule time lookup, which compares a new event's title against the group's own recent titles — and this design deliberately copies its shape rather than the meal catalogue's, because a household's task text and a public recipe catalogue have opposite privacy postures.
In scope: what text leaves the device and when, where it is processed, what (if anything) is stored, how the group's privacy level routes or refuses the call, deletion, children, and cost. Out of scope, covered elsewhere: the general residency position and spend ceilings (the cloud-inference assessment); the search interface (paging, filters, history lists), which moves no new data.
2. Data involved
| Kind | Whose | Children's? | Sensitive? | Free text? |
|---|---|---|---|---|
| The query the member types | the member's own words | possibly — a query can name a child ("Ada dentist") | possibly — health-adjacent errands are ordinary task language | yes |
| Candidate task titles and notes, sent for comparison on a submit | the group's — written by any member | often — tasks in a family group are frequently about children | possibly — "pick up Ada's antibiotics" is a task title | yes |
| The result | a ranked list of task identifiers the member can already read | — | — | no |
Not collected or created, and these absences carry the design:
- No stored index. Embeddings are computed per request and discarded. There is no vector table, no cached representation of a household's task text at rest, nothing a deletion has to chase. Deleting a task deletes everything search knew about it, because search knew nothing.
- No query history. The query is not stored server-side, not logged, and not used to train or tune anything. What is metered is token counts, which carry no words.
- No result log. Which tasks matched is computed, returned, and forgotten.
- Only titles and notes are candidates. Not assignees' names as searchable identity, not completion history, not anything from the baby tracker. Widening the candidate set is a review trigger (§9).
3. Flows and locations
| Data | Device | Supabase (Sydney, ap-southeast-2) | GCP Vertex AI (australia-southeast1) | Region |
|---|---|---|---|---|
| Typing-time matching | entirely on the device, over rows already loaded | — | — | the phone |
| A submitted semantic query | sent from the device with the member's own session | our assistant server reads the group's tasks through the member's own access, checks the group's privacy level, and meters the spend | the query text and the candidate titles/notes are embedded; similarity is computed on our server; nothing is retained by us | Sydney, Australia |
- Embeddings are not a conversation. An embedding model turns text into numbers for comparison; it follows no instructions, so a task title that says "ignore all previous instructions" is just a badly named task. The prompt-injection defences the assistant needs do not apply here — and this stays true only while no generative step is added on top (§9).
- Privacy-level routing, decided on the server per request, never from the client's claim:
- Device-only: the semantic submit does not exist. The search field still works — the typing-time mode is local — and, since 30 September 2026, pressing Search finds tasks with a similar spelling among those on the phone, worked out on the phone with nothing sent (search and suggested times on the phone). Sending a Device-only group's task text to any server for comparison would break the level's whole promise, and the refusal fails closed: an unreadable level is treated as Device-only, and when the server refuses on that ground the phone answers with its spelling search instead.
- Australia and both Global levels: the call runs, on the Sydney embedding endpoint. Task text is group information, so under our residency rule it is processed in Australia at every level — the wider Global levels widen what the assistant may do, not where this lookup runs.
- Encrypted tasks are opened for that one request, and only there (revised 30 September 2026, when a task's words began to be encrypted at every level — §3.1). A row whose words are encrypted holds only a placeholder in the database. Our server opens the candidates' titles and notes inside the member's request, with the copy of the group's key kept for the assistant, records that it did so, compares them, and keeps nothing it opened. A row it cannot open — the group has no copy of its key on our side, or the row is under a key that copy does not reach — is left out of the candidates rather than compared as the word "Encrypted", and the scope line says the search covers what could be read.
- Logs: request outcomes and token counts. Never the query, never a title.
3.1 What the candidate text's protection actually is today
A task's title and notes are still stored in plain text in our database today, for every group except a Device-only one. We have built, but not yet released, sealing a task's words on the phones at every level (see the content encryption assessment in this register): new and edited tasks as they are saved, older ones once a member runs "Encrypt older tasks", once released. This assessment said that when task notes were encrypted it would be re-read, and that sealed content would be excluded from search rather than opened. It was re-read, and that second sentence was changed on purpose. Excluding encrypted tasks would make search find nothing at all in a group whose history had been encrypted — a search box that answers "no match" for every question is a wrong answer, not a private one. What the change will do and will not do, once released:
- For an Australia or Global group, the words compared will be the same words the feature compares today, sent to the same Sydney endpoint, under the same caps, on the same explicit submit, and kept by nobody. What will change is where they rest: ciphertext in the database, opened by our assistant server only inside the member's own request, through the one piece of our code that may open encrypted group content, with a record of each opening. An operator holding only our database will no longer be able to read them.
- For a Device-only group the feature still will not run at all (§3). There is no copy of the group's key on our side to open anything with, and the phone's spelling search stands in.
- What would change this assessment again (§9): opening anything other than the candidate titles and notes; keeping anything opened beyond the request; or opening encrypted content for any caller other than a member's own submit.
3.2 What bounds a request, beyond the money
The spend ledger stops a runaway costing money; it does not, on its own, stop a single request being enormous, and a request's size is a privacy question as well as a cost one — the more of a household's task text travels per submit, the more a single accidental or hostile call discloses. The design bounds each request in its own right:
- A cap on the candidate set, in the same spirit as the schedule time lookup's roughly 150 recent titles: a bounded, recent, relevant window of the group's tasks, not the group's entire history. Where the cap bites, the screen says the search covered recent tasks rather than implying completeness — an unstated truncation reads as "nothing matched", which is a wrong answer dressed as an empty one.
- A cap on the query's own length, checked on the server rather than trusted from the client, so a submit cannot carry an essay.
- A cap on candidate text per row, so one task whose notes are a pasted document cannot dominate a request.
- One in-flight semantic request per member, with a new submit cancelling the previous one — which is the interaction the existing meal search already models, and which doubles as the rate limit: a member cannot hold ten of these open by pressing Return repeatedly.
- Cost: on top of those bounds, the submit is metered against the member's own weekly allowance and the platform's spend ledger, exactly like the schedule time lookup. Typing is free because typing sends nothing. There is no per-keystroke call by construction, not by throttling.
The caps were deliberately specified as requirements with reasons rather than as numbers before the build: the numbers belong with the build, and this assessment is what says a build without them is not the design that was assessed.
As built (18 September 2026): the server fetches at most 150 candidate rows — plus one, so it can tell the screen the set was truncated rather than let an incomplete search read as "no match"; a query longer than 200 characters is refused; each candidate's text is cut to 300 characters before it is compared; at most 20 results come back, and only those whose similarity to the query reaches 0.55; a member holds one semantic request at a time — a second submit while one is open is refused with a "try again in a moment" answer rather than queued. Sealed titles are excluded before ranking whenever they could not be opened; since 30 September 2026 the rest are opened in the request, after the two spend pre-flights and before ranking, and nothing opened is kept. Every cap is applied on the server, whatever the app declared. The order of the server's checks — sign-in, rate limit, query shape, the child-account refusal, the group's privacy level, the candidate read through the member's own access, the two spend pre-flights, the ranking, and the charge recorded only after the work is done — is pinned by an automated check that fails the build if any step is removed or reordered, and a second check refuses any log line in that handler that could carry the query or a title.
4. Legal basis and consents
No new consent purpose. The member's own explicit submit is the act — the same basis as typing
a question to the assistant, and narrower: nothing is generated and the processing stays in
Australia. The privacy policy's existing description of model processing covers task text
reaching a model on a member's request, and its wording was found to enumerate surfaces — so
this precondition is now satisfied: CURRENT_PRIVACY_POLICY_VERSION v1.7-2026-09-18 (§5) names
this feature by name, dated the day this assessment was revised. No switch is added: the
group-level control is the privacy level itself, which already expresses "keep our words off
servers" (Device-only) and is enforced on this path server-side.
5. Risks and mitigations
| Risk | Who is harmed | Mitigation | Structural or conventional? | Residual |
|---|---|---|---|---|
| A member assumes typing is already "AI search" and that every keystroke is leaving the phone — or the reverse: assumes Return is still local and sends a sensitive query without meaning to | the member, and whoever the query names | The two modes are visibly different acts (typing filters instantly; Return is labelled as search-by-meaning), and the semantic call happens only on submit — there is no code path from a keystroke to the network | Structural for submit-only; conventional for the labelling | A member can still submit words they regret. The query is not stored, which bounds it |
| A Device-only group's task text reaches a server because a client was old, modified, or lied about the level | the whole group — the level's promise is broken silently | The level is read server-side per request through the member's own access, and refusal fails closed on an unreadable level — the same posture every other model surface already takes | Structural | — |
| A stored index is added later "for performance" and becomes household text at rest that deletion no longer reaches | every member; children named in tasks, whose records outlive the task | This design has no index, and §9 makes adding one a re-assessment with named obligations (deletion cascade, sealed-row exclusion, at-rest posture) rather than an optimisation | Conventional — a documented gate | Accepted: nothing stops a future change technically; the gate is this register |
| The candidate set quietly widens — completed history, baby records, chat — because "search should find everything" | whoever wrote or is named in the newly included text | The candidate columns are an explicit allowlist (titles and notes), asserted by test, and §9 names widening as a trigger | Conventional, test-pinned | — |
| The query itself is free text about a person, processed by a provider | whoever is named | Processing is in Australia, per-request, not retained by us, and the provider does not train on it (per our standing terms); the query is never logged | Structural for the region pin (the endpoint is a constant, not configuration); conventional for the no-logging discipline | Accepted — the same acceptance as every Sydney surface, stated rather than hidden |
| A single submit carries far more of the group's task text than the member imagines — a scripted or accidental loop turns a search box into a bulk export of a household's tasks to a provider | every member, and whoever their tasks name | Per-request caps on the candidate set, the query length and the text taken from any one row, all checked on the server; one in-flight request per member, a new submit cancelling the last; the weekly allowance and spend ledger underneath all of it (§3.2) | Structural for the server-side caps and the metering; conventional for the screen saying when the candidate window truncated | Accepted: a member can still submit repeatedly within their allowance. Nothing is retained at either end, which is what bounds the consequence |
| Results imply tasks that do not exist, or leak rows the member cannot read | the member misled; the group if access widened | Results are task identifiers resolved against rows fetched through the member's own access — a similarity score cannot mint a row, and a row the member cannot read is never in the candidate set to begin with | Structural — the member's own access is the fetch path | — |
6. Structural versus conventional
Structural: the semantic call exists only behind an explicit submit; the privacy level is read server-side per request and fails closed; candidates are fetched through the member's own access (row security decides visibility, so search cannot see more than the member); an encrypted row's words are opened only by the one part of our server that may open group content, only inside the member's request, with a record of each opening, and a row it cannot open is left out rather than compared as a placeholder; the Sydney endpoint is a constant in the code rather than a setting; nothing is stored, so retention and deletion questions have nothing to attach to; the per-request caps on the candidate set, the query's length and the text taken from any one row are applied on the server rather than trusted from the client; the spend is metered by the same ledger as every other call.
Conventional, each to be test-pinned in the build: the candidate-column allowlist; the scope sentence on the search screen, including saying when the candidate window truncated; the no-query-in-logs discipline; the child-account gate (§7); one in-flight request per member; the absence of any generative step over the results.
7. Best interests of the child
Two different children are involved: a child named in task text, and a child using the search.
- A child named in tasks is why the candidate text is treated as sensitive-adjacent even though a task list sounds mundane: a family's tasks are a running description of its children's appointments, medicines and school life. The mitigations are the region pin, the per-request no-retention shape, and the absence of any index — a child's errand is compared and forgotten, not profiled.
- A child using the search gets the typing-time mode only, in this first version. Today no surface a child account operates sends anything to a model — the child variant has no assistant, no voice, and the daily brief is refused for them server-side — and search does not become the first. The cost to the child is a weaker search than an adult's; the benefit is that the line "a child's own use of the app reaches no model" stays simple, checkable and true. This gate is conventional (a client check backed by a server-side refusal to be added with the feature), and we state that plainly rather than claiming more.
- The safer design considered and taken: per-request embeddings over a stored index — the version of this feature that creates no new record of anybody's childhood.
8. Residual risk we accept
- Task text crosses to a model provider on every submit, in Australia, per-request. This is the platform's standing position for group words on a member's own ask, applied to one more surface, and it is why the feature is absent for Device-only groups rather than degraded.
- Our server can open a group's task words while a member searches (§3.1). Since 30 September 2026 those words are encrypted at rest, and search opens them in the request with the copy of the group's key kept for the assistant. That copy is what makes this possible at all, and it is the same copy that already opens event notes for the assistant; a group that wants no such copy chooses Device-only and gets search on the phone instead. Older tasks stay readable in the database until a member encrypts them.
- A member can submit repeatedly within their own allowance. The caps bound each request; nothing bounds a determined member beyond the weekly meter. Accepted, because the words are their own group's and nothing is retained at either end.
- The child gate and the candidate allowlist are conventions. Tests will pin them; tests can be deleted. The register is the memory.
- Local search quality is the floor. For Device-only groups and children the plain-text mode is the whole feature. We accept a worse search rather than a wider flow.
- A misspelled or oblique query can still name a person ("sarahs surgery followup"), and no design of ours can classify that away without reading it. Bounded by non-retention.
9. Review trigger
Re-read and revise this assessment before building any of:
- a stored index or vector table over household text — the at-rest, deletion-cascade and sealed-content analysis in §2/§3 no longer holds and must be redone;
- any generative step over the results (re-ranking by a language model, summarising matches, "answering" the query) — the injection analysis in §3 flips from not-applicable to required, and the untrusted-text fencing rules apply in full;
- widening the candidate set beyond task titles and notes, or adding a second consumer of the same lookup;
- offering the semantic mode to child accounts, or to Device-only groups in any form;
- the search covering another group's rows for any reason;
- task titles or notes becoming sealed content for groups that are not Device-only (§3.1) — the candidate set shrinks to what remains readable, and sealed text is excluded rather than opened;
- removing or materially raising any of the per-request caps in §3.2, which are what keep one submit from being a bulk disclosure.
Revised against the shipped code, 18 September 2026. Implemented as designed; no deviation was found. What the build added beyond the draft is only the numbers in §3.2 and the two automated checks that keep them and the server's order of refusals in place. None of the triggers above has fired: there is still no stored index, no generative step, no widening of the candidate set, and child accounts and Device-only groups still get local search only.
Decided, 30 September 2026. Search by meaning stays on the server, as described here, for groups at the Australia and Global levels. Device-only groups get a spelling search on the phone when they press Search (§3), which sends nothing and is described in search and suggested times on the phone; that is not a semantic mode, so the trigger above about Device-only groups has not fired. No model is added to the app. Child accounts in other groups still get local search only. The paragraph below is the plan as it stood before the decision.
Planned move to the phone, 30 September 2026. Search by meaning is planned to run on the phone, over the tasks the phone already holds, so that pressing Search sends nothing — see search and suggested times on the phone. When that ships, this assessment changes in four places: §3's server flow is gone for app versions that have the phone-side search; the child-account and Device-only refusals in §3 and §7 no longer have a reason, because nothing leaves the phone, and the phone-side search is offered to both; §3.2's caps stop mattering for those versions; and the server route stays only for older app versions, then is switched off. Until then nothing here has changed. The first evaluation found that without a model the phone-side search finds noticeably less than this server version does for questions about meaning, and whether to add a model to the app for it is still open — so the server version stays in place for now.
4 November 2026 — routine-edit lookup. Routine-edit lookup is a second consumer of the
same lookup. It sends titles only, without notes, under the same caps, to the same region,
with the same refusals. Ranked titles go back to the assistant's model. That is a generative
step over the results, so they are fenced as untrusted data (ROUTINE_RESULT). Both review
triggers — a second consumer and a generative step — apply. Assessed in
routines §10.
10. Checklist against exposure draft s 38(2)
| s 38(2) | Requires | Answered in |
|---|---|---|
| (a) | nature, scope, context, flow and purposes of handling children's personal information | §§1–3 |
| (b) | why the collection is strictly necessary | §2 — nothing new is collected; existing rows are compared on the member's own ask and nothing is retained |
| (c) | lawful and fair collection | §4 |
| (d) | consistency with the best interests of the child | §7 |
| (e) | how we comply with the Code and the Act | §4 and §6 |
| (f) | risk of harm to children | §5, §7 |
s 38(3): this assessment precedes the build. s 39: listed on the published register.
11. Sources
Primary: the Children's Online Privacy Code exposure draft, ss 10, 26, 38, 39.
In repo: agent-server/src/routineMatch.ts (routine-edit lookup) and
agent-server/src/domains/choreAgent.ts (untrusted routine-result fencing);
agent-server/src/server.ts and the schedule time-lookup route (the per-request
embedding shape this copies, and the child-account refusal); agent-server/src/vertex.ts (the
Sydney region constant and the reasoning that it must not become configuration);
agent-server/src/privacyLevel.ts (readPrivacyLevel, isLockedLevel — the fail-closed level
read); docs/pia/cloud-inference.md §§2–3 (embeddings carry group information; the residency
test is the request body); docs/pia/privacy-levels.md (what Device-only promises);
components/Meals/ExplorePlans.tsx (the local-then-submit interaction precedent — and the
catalogue/household distinction that stops its server design being copied); our internal
engineering rules on identifiers in logs being data an erasure cannot reach, and on household
data being processed in Australia.